From 98d4e351763441fe938089352d65f97e6845636b Mon Sep 17 00:00:00 2001 From: i063052 Date: Fri, 20 Dec 2024 17:27:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[HCPSDKFIORIUIKIT-2683]?= =?UTF-8?q?=20[SwiftUI]=20Slider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Examples.xcodeproj/project.pbxproj | 10 +- .../FioriSwiftUICore/CoreContentView.swift | 6 + .../Slider/SliderExample.swift | 802 ++++++++++++++++++ .../DataTypes/FioriSlider+DataType.swift | 660 ++++++++++++++ .../BaseComponentProtocols.swift | 40 + .../CompositeComponentProtocols.swift | 222 +++++ .../_FioriStyles/ActiveTrackStyle.fiori.swift | 35 + .../_FioriStyles/FioriSliderStyle.fiori.swift | 523 ++++++++++++ .../InactiveTrackStyle.fiori.swift | 35 + .../LeadingAccessoryStyle.fiori.swift | 35 + .../_FioriStyles/LowerThumbStyle.fiori.swift | 35 + .../RangeSliderControlStyle.fiori.swift | 288 +++++++ .../TrailingAccessoryStyle.fiori.swift | 35 + .../_FioriStyles/UpperThumbStyle.fiori.swift | 35 + .../ActiveTrack/ActiveTrack.generated.swift | 63 ++ .../ActiveTrackStyle.generated.swift | 28 + .../FioriSlider/FioriSlider.generated.swift | 371 ++++++++ .../FioriSliderStyle.generated.swift | 83 ++ .../InactiveTrack.generated.swift | 63 ++ .../InactiveTrackStyle.generated.swift | 28 + .../LeadingAccessory.generated.swift | 57 ++ .../LeadingAccessoryStyle.generated.swift | 28 + .../LowerThumb/LowerThumb.generated.swift | 63 ++ .../LowerThumbStyle.generated.swift | 28 + .../RangeSliderControl.generated.swift | 134 +++ .../RangeSliderControlStyle.generated.swift | 53 ++ .../TrailingAccessory.generated.swift | 57 ++ .../TrailingAccessoryStyle.generated.swift | 28 + .../UpperThumb/UpperThumb.generated.swift | 63 ++ .../UpperThumbStyle.generated.swift | 28 + ...entStyleProtocol+Extension.generated.swift | 448 ++++++++++ .../EnvironmentVariables.generated.swift | 168 ++++ .../ModifiedStyle.generated.swift | 224 +++++ .../ResolvedStyle.generated.swift | 128 +++ ...yleConfiguration+Extension.generated.swift | 12 + .../View+Extension_.generated.swift | 136 +++ ...iewEmptyChecking+Extension.generated.swift | 60 ++ .../en.lproj/FioriSwiftUICore.strings | 60 ++ 38 files changed, 5171 insertions(+), 1 deletion(-) create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/Slider/SliderExample.swift create mode 100644 Sources/FioriSwiftUICore/DataTypes/FioriSlider+DataType.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/ActiveTrackStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/FioriSliderStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/InactiveTrackStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/LeadingAccessoryStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/LowerThumbStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/RangeSliderControlStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/TrailingAccessoryStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/UpperThumbStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrack.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrackStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSlider.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSliderStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrack.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrackStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessory.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessoryStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumb.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumbStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControl.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControlStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessory.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessoryStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumb.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumbStyle.generated.swift diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 4dba94588..ce825faa4 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -408,6 +408,10 @@ E99A025E2B9EC055008A4B77 /* SearchIconAndPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchIconAndPlaceholder.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 3B660C982D156B7000E92505 /* Slider */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Slider; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 1F60179129A8439A00DBDCDE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -694,6 +698,7 @@ B1D41B1E291A2D2E004E64A5 /* Picker */, B8239D8626815C11003455D2 /* ContactItem */, 692F338926556A34009B98DA /* SideBar */, + 3B660C982D156B7000E92505 /* Slider */, 1FF3662C264C662A00AB8BD8 /* DimensionSelector */, 97CEF8A92553D6F1008BFBEF /* SignatureView */, AB988B11263128C400483D87 /* DataTable */, @@ -1023,6 +1028,9 @@ dependencies = ( 1F6017A029A8439C00DBDCDE /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 3B660C982D156B7000E92505 /* Slider */, + ); name = Examples; packageProductDependencies = ( 8A4A31B824E3564F00B63AF0 /* FioriSwiftUI */, diff --git a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift index 1f26f2d50..155298602 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift @@ -192,6 +192,12 @@ struct CoreContentView: View { { Text("Step Progress Indicator") } + + NavigationLink( + destination: SliderExample()) + { + Text("Slider") + } } Section(header: Text("Pickers")) { diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Slider/SliderExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Slider/SliderExample.swift new file mode 100644 index 000000000..ecaa773c3 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/Slider/SliderExample.swift @@ -0,0 +1,802 @@ +import Foundation + +import FioriSwiftUICore +import FioriThemeManager +import SwiftUI + +struct SliderExample: View { + var body: some View { + List { + NavigationLink { + SingleSliderExample() + } label: { + Text("Slider") + } + NavigationLink { + RangeSliderExample() + } label: { + Text("Range Slider") + } + NavigationLink { + CustomSliderExample() + } label: { + Text("Customized Slider") + } + } + } +} + +struct SingleSliderExample: View { + @State var intValue: Double = 20 + @State var wrapperValue: Double = 20 + @State var decimalValue: Double = 14.0 + @State var distanceValue: Double = 12450.68 + @State var longValue: Double = 3.0 + @State var statusValue: Double = 350 + @State var disableValue: Double = 60 + @State var disableEditValue: Double = 60 + @State var limitValue: Double = 30 + @State var volumeValue: Double = 15.0 + @State var lettersValue: Double = 6 + @State var feedbackValue: Double = 10 + + var statusDescription: AttributedString { + if self.statusValue > 250 { + "The mouse scroll speed is too fast." + } else if self.statusValue > 150 { + "The mouse scroll speed is a bit fast." + } else if self.statusValue > 50 { + "The mouse scroll speed is just right." + } else { + "" + } + } + + var infoStyle: any InformationViewStyle { + if self.statusValue > 250 { + InformationViewErrorStyle.error + } else if self.statusValue > 150 { + InformationViewWarningStyle.warning + } else if self.statusValue > 50 { + InformationViewInformationalStyle.informational + } else { + InformationViewFioriStyle.fiori + } + } + + var onValueChange: (Bool, Double, Int) -> Void = { isEditing, value, decimalPlace in + if !isEditing { + print("Slider value change to: " + String(format: "%.\(decimalPlace)f", value)) + } + } + + var body: some View { + List { + let title1 = "Integer value Slider" + let description1 = "The Integer Value Slider allows you to select whole numbers within a specified range" + FioriSlider( + title: AttributedString(title1), + value: self.$intValue, + description: AttributedString(description1), + onValueChange: { isEditing, newValue in + self.onValueChange(isEditing, newValue, 0) + } + ) + .accessibilityAdjustments(title: title1, description: description1) + + let title2 = "Decimal value Slider" + let description2 = "The Decimal Value Slider offers the flexibility to select numbers with decimal precision" + FioriSlider( + title: AttributedString(title2), + value: self.$decimalValue, + step: 2.5, + decimalPlaces: 1, + description: AttributedString(description2) + ) + .accessibilityAdjustments(title: title2, description: description2) + + let kmDecimalPlace = 2 + let format = "%.\(kmDecimalPlace)f KM" + let title3 = "Distance" + FioriSlider( + title: AttributedString(title3), + value: self.$distanceValue, + range: 30 ... 60000, + step: 20.55, + decimalPlaces: kmDecimalPlace, + valueFormat: format, + leadingValueFormat: format, + trailingValueFormat: format, + onValueChange: { isEditing, newValue in + self.onValueChange(isEditing, newValue, 2) + } + ) + .accessibilityAdjustments(title: title3) + + let title4 = "The Integer Value Slider allows you to select whole numbers within a specified range. This type of slider is ideal for settings where precise, non-fractional values are required. Whether you are setting quantities, levels, or steps, the Integer Value Slider ensures that you can quickly and easily choose a precise whole number." + FioriSlider( + title: AttributedString(title4), + value: self.$longValue, + step: 10, + onValueChange: { isEditing, newValue in + self.onValueChange(isEditing, newValue, 0) + } + ) + .accessibilityAdjustments(title: title4) + + let title5 = "Mouse Scrolling Speed" + FioriSlider( + title: "Mouse Scrolling Speed", + value: self.$statusValue, + range: 0 ... 400, + step: 10, + description: self.statusDescription, + onValueChange: { isEditing, newValue in + self.onValueChange(isEditing, newValue, 0) + } + ) + .accessibilityAdjustments(title: title5, description: String(self.statusDescription.characters)) + .informationViewStyle(self.infoStyle).typeErased + + // Disable Slider + let title6 = "Disabled Slider" + let description6 = "The slider is currently disabled." + FioriSlider(title: AttributedString(title6), value: self.$disableValue, description: AttributedString(description6)) + .accessibilityAdjustments(title: title6, description: description6) + .environment(\.isEnabled, false) + + // Custom Slider + let title7 = "Volume" + let description7 = "Adjust the volume by moving the slider" + FioriSlider( + titleView: { Text(title7) }, + value: self.$volumeValue, + range: 0 ... 30, step: 2.0, + description: AttributedString(description7), + valueLabelView: { + if self.volumeValue <= 0 { FioriIcon.actions.soundOff } + else if self.volumeValue >= 30 { FioriIcon.actions.soundLoud } + else { EmptyView() } + }, + leadingAccessory: { FioriIcon.actions.sound }, + trailingAccessory: { FioriIcon.actions.soundLoud } + ) + .accessibilityAdjustments(title: title7, description: description7) + .valueLabelStyle { configuration in + configuration.valueLabel + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(.tint) + } + .leadingAccessoryStyle { configuration in + configuration.leadingAccessory + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(.tint) + } + .trailingAccessoryStyle { configuration in + configuration.trailingAccessory + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(.tint) + } + + let title8 = "Letters" + let letterValue = getLetter(for: Int(String(format: "%.0f", lettersValue)) ?? 1) + let leadingLabel = getLetter(for: 1) + let trailingLabel = getLetter(for: 26) + FioriSlider( + titleView: { Text(title8) }, + value: self.$lettersValue, + range: 1 ... 26, + valueLabelView: { + Text(letterValue) + }, + leadingAccessory: { + Text(leadingLabel) + }, + trailingAccessory: { + Text(trailingLabel) + } + ) + .accessibilityAdjustments(title: title8, value: letterValue, leadingLabel: leadingLabel, trailingLabel: trailingLabel) + + FioriSlider( + value: self.$limitValue, + range: 1 ... 80, + step: 5, + description: "Simple Custom Slider", + showsValueLabel: false, + leadingAccessory: { + Text("Limiter") + .font(Font.fiori(forTextStyle: .body)) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + }, + trailingAccessory: { + Text("\(String(format: "%.0f", self.limitValue))") + } + ) + .accessibilityAdjustments(title: "Limiter", description: "Simple Custom Slider") + + let title9 = "\(String(format: "%.0f", self.feedbackValue)) Stories" + let description9 = "User Stories" + FioriSlider( + titleView: { Text(title9) }, + value: self.$feedbackValue, + description: AttributedString(description9), + showsValueLabel: false + ) + .accessibilityAdjustments(title: title9, description: description9) + } + .listStyle(.plain) + } +} + +struct RangeSliderExample: View { + @State var intLowerValue: Double = 10 + @State var intUpperValue: Double = 60 + + @State var lowerValue: Double = 30.0 + @State var upperValue: Double = 254.5 + + @State var twoDecimalLowerValue: Double = 28.97 + @State var twoDecimalUpperValue: Double = 79.23 + + @State var singleEditUpperValue: Double = 40 + @State var disableEditValue: Double = 20 + + @State var disabledLowerValue: Double = 5.0 + @State var disabledUpperValue: Double = 20.0 + + @State var customLowerValue: Double = 4 + @State var customUpperValue: Double = 16 + + @State var colorLowerValue: Double = 0.1987 + @State var colorUpperValue: Double = 0.8796 + + var onRangeValueChange: (Bool, Double, Double, Int) -> Void = { isEditing, lowerValue, upperValue, decimalPlace in + if !isEditing { + print("Range Slider value was: " + String(format: "%.\(decimalPlace)f", lowerValue) + " - " + String(format: "%.\(decimalPlace)f", upperValue)) + } + } + + var body: some View { + List { + let singleEditableRange = 1.0 ... 70.0 + FioriSlider( + title: AttributedString("Single Range Slider (\(String(format: "%.0f", singleEditableRange.lowerBound)) - \(String(format: "%.0f", singleEditableRange.upperBound)))"), + upperValue: self.$singleEditUpperValue, + range: singleEditableRange, + description: getInfoDescription(lowerValue: singleEditableRange.lowerBound, upperValue: self.singleEditUpperValue, range: singleEditableRange, decimalPlace: 0, defaultDesc: "The single editable slider accepts integer numbers."), + onValueChange: { isEditing, value in + if !isEditing { + print("The Slider value was: " + String(format: "%.0f", value)) + } + } + ) + .informationViewStyle(getInfoStyle(lowerValue: singleEditableRange.lowerBound, upperValue: self.singleEditUpperValue, range: singleEditableRange)).typeErased + + FioriSlider( + title: "Range Slider (0 - 100)", + lowerValue: self.$intLowerValue, + upperValue: self.$intUpperValue, + description: getInfoDescription(lowerValue: self.intLowerValue, upperValue: self.intUpperValue, range: 0 ... 100, decimalPlace: 0, defaultDesc: "A range slider that allows users to input integers for the lower and upper values provides flexibility in defining numeric ranges through both sliding handles and direct text input."), + onRangeValueChange: { isEditing, lowerValue, upperValue in + self.onRangeValueChange(isEditing, lowerValue, upperValue, 0) + } + ) + .informationViewStyle(getInfoStyle(lowerValue: self.intLowerValue, upperValue: self.intUpperValue, range: 0 ... 100)).typeErased + + let oneDecimalRange = 10.5 ... 400.5 + FioriSlider( + title: AttributedString("Range Slider (\(String(format: "%.1f", oneDecimalRange.lowerBound)) - \(String(format: "%.1f", oneDecimalRange.upperBound)))"), + lowerValue: self.$lowerValue, + upperValue: self.$upperValue, + range: 10.5 ... 400.5, + step: 3.5, + decimalPlaces: 1, + description: getInfoDescription(lowerValue: self.lowerValue, upperValue: self.upperValue, range: 10.5 ... 400.5, decimalPlace: 1, defaultDesc: "The range slider accepts numbers with one decimal place."), + onRangeValueChange: { isEditing, lowerValue, upperValue in + self.onRangeValueChange(isEditing, lowerValue, upperValue, 1) + } + ) + .informationViewStyle(getInfoStyle(lowerValue: self.lowerValue, upperValue: self.upperValue, range: 10.5 ... 400.5)).typeErased + + let twoDecimalRange = 1.58 ... 90.58 + FioriSlider( + title: AttributedString("Range Slider (\(String(format: "%.2f", twoDecimalRange.lowerBound)) - \(String(format: "%.2f", twoDecimalRange.upperBound)))"), + lowerValue: self.$twoDecimalLowerValue, + upperValue: self.$twoDecimalUpperValue, + range: twoDecimalRange, + step: 0.01, + decimalPlaces: 2, + description: getInfoDescription(lowerValue: self.twoDecimalLowerValue, upperValue: self.twoDecimalUpperValue, range: twoDecimalRange, decimalPlace: 2, defaultDesc: "The range slider accepts numbers with two decimal place."), + onRangeValueChange: { isEditing, lowerValue, upperValue in + self.onRangeValueChange(isEditing, lowerValue, upperValue, 2) + } + ) + .informationViewStyle(getInfoStyle(lowerValue: self.twoDecimalLowerValue, upperValue: self.twoDecimalUpperValue, range: twoDecimalRange)).typeErased + + FioriSlider( + title: AttributedString("Range Slider (0-50)"), + lowerValue: self.$disabledLowerValue, + upperValue: self.$disabledUpperValue, + range: 0.0 ... 50.0, + description: "Disabled range slider" + ).disabled(true) + + FioriSlider( + title: AttributedString("Slider (10-80)"), + upperValue: self.$disableEditValue, + range: 10 ... 80, + description: "Disabled slider" + ).disabled(true) + + let leadingLabel = getLetter(for: Int(self.customLowerValue)) + let trailingLabel = getLetter(for: Int(self.customUpperValue)) + let rangeFormat = (getLetter(for: 1), getLetter(for: 26)) + FioriSlider( + title: "Letters", + lowerValue: self.$customLowerValue, + upperValue: self.$customUpperValue, + range: 1 ... 26, + description: "A simple, custom range slider to select a range of letters from lower to upper.", + rangeFormat: rangeFormat, + leadingAccessory: { + Text(leadingLabel).accessibilityHidden(true) + }, + leadingValueFormat: leadingLabel, + trailingAccessory: { + Text(trailingLabel).accessibilityHidden(true) + }, + trailingValueFormat: trailingLabel + ) + + let step = 0.00001 + FioriSlider( + lowerValue: self.$colorLowerValue, + upperValue: self.$colorUpperValue, + range: 0 ... 1, + step: step, + decimalPlaces: 5, + description: "A custom range slider allows you to change the color of the profile image.", + valueLabelView: { + ColorDemoViewStyle(lowerValue: self.$colorLowerValue, upperValue: self.$colorUpperValue) + .accessibilityHidden(true) + }, + leadingAccessory: { + ColorPickerView(value: self.$colorLowerValue, step: step) + }, + trailingAccessory: { + ColorPickerView(value: self.$colorUpperValue, step: step) + } + ) + } + .listStyle(.plain) + } +} + +struct CustomSliderExample: View { + @State private var intValue: Double = 20 + @State private var customIntValue: Double = 40 + @State private var disableValue: Double = 30 + + @State var intLowerValue: Double = 10 + @State var intUpperValue: Double = 60 + + @State var disabledLowerValue: Double = 5.0 + @State var disabledUpperValue: Double = 20.0 + + @State var singleEditUpperValue: Double = 40 + + @State var disableEditValue: Double = 20 + + @State var isEditFieldFocused1: Bool = false + @State var isEditFieldFocused2: Bool = false + + @State var customThumbLowerValue: Double = 10 + @State var customThumbUpperValue: Double = 60 + + var body: some View { + List { + let title1 = "Integer value Slider" + let description1 = "The Integer Value Slider allows you to select whole numbers within a specified range" + FioriSlider( + title: AttributedString(title1), + value: self.$intValue, + description: AttributedString(description1) + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(.cyan) + .font(.fiori(forTextStyle: .title3, weight: .regular)) + } + .valueLabelStyle { configuration in + configuration.valueLabel.foregroundStyle(Color.preferredColor(.accentLabel6)) + .font(.fiori(forTextStyle: .title2)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .body, weight: .regular)) + } + .leadingAccessoryStyle { configuration in + configuration.leadingAccessory.foregroundStyle(Color.preferredColor(.accentLabel4)) + .font(.fiori(forTextStyle: .title1)) + } + .trailingAccessoryStyle { configuration in + configuration.trailingAccessory.foregroundStyle(Color.red) + .font(.fiori(forTextStyle: .title1)) + } + .upperThumbStyle { configuration in + configuration.upperThumb.foregroundStyle(Color.orange) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.green) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.red) + } + + FioriSlider( + title: AttributedString("Custom Standard Slider"), + value: self.$customIntValue, + description: AttributedString("Customize the strand slider with thumb, track"), + thumb: Ellipse() + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .title2)) + } + .valueLabelStyle { configuration in + configuration.valueLabel.foregroundStyle(Color.preferredColor(.pink6)) + .font(.fiori(forTextStyle: .body)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .body, weight: .regular)) + } + .leadingAccessoryStyle { configuration in + configuration.leadingAccessory.foregroundStyle(Color.preferredColor(.grey4)) + .font(.fiori(forTextStyle: .title2)) + } + .trailingAccessoryStyle { configuration in + configuration.trailingAccessory.foregroundStyle(Color.orange) + .font(.fiori(forTextStyle: .title2)) + } + .upperThumbStyle { configuration in + configuration.upperThumb + .frame(height: 40) + .foregroundStyle(Color.preferredColor(.pink6)) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.brown) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.black) + } + + let title6 = "Disabled Slider" + let description6 = "The slider is currently disabled." + FioriSlider(title: AttributedString(title6), value: self.$disableValue, description: AttributedString(description6)) + .accessibilityAdjustments(title: title6, description: description6) + .titleStyle { configuration in + configuration.title.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .title2, weight: .regular)) + } + .valueLabelStyle { configuration in + configuration.valueLabel.foregroundStyle(Color.preferredColor(.accentLabel6)) + .font(.fiori(forTextStyle: .title3)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(Color.preferredColor(.mango11)) + .font(.fiori(forTextStyle: .title2, weight: .regular)) + } + .leadingAccessoryStyle { configuration in + configuration.leadingAccessory.foregroundStyle(Color.preferredColor(.mango4)) + .font(.fiori(forTextStyle: .title3)) + } + .trailingAccessoryStyle { configuration in + configuration.trailingAccessory.foregroundStyle(Color.green) + .font(.fiori(forTextStyle: .title3)) + } + + .environment(\.isEnabled, false) + + let trailingEditFieldStyle = FioriSliderTextFieldStyle(borderColor: Color.yellow, focusedBorderColor: Color.black, font: Font.fiori(forTextStyle: .title2), foregroundColor: Color.blue) + FioriSlider( + title: "Range Slider (0 - 100)", + lowerValue: self.$intLowerValue, + upperValue: self.$intUpperValue, + description: getInfoDescription(lowerValue: self.intLowerValue, upperValue: self.intUpperValue, range: 0 ... 100, decimalPlace: 0, defaultDesc: "A range slider that allows users to input integers for the lower and upper values provides flexibility in defining numeric ranges through both sliding handles and direct text input."), + onEditFieldFocusStatusChange: { + self.isEditFieldFocused1 = $0 + } + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(self.isEditFieldFocused1 ? trailingEditFieldStyle.focusedBorderColor : .brown) + .font(.fiori(forTextStyle: .title2, weight: .regular)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(Color.preferredColor(.mango11)) + .font(.fiori(forTextStyle: .body, weight: .bold)) + } + .lowerThumbStyle { configuration in + configuration.lowerThumb.foregroundStyle(Color.green) + } + .upperThumbStyle { configuration in + configuration.upperThumb.foregroundStyle(Color.orange) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.green) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.red) + } + .leadingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle(borderColor: Color.orange, focusedBorderColor: trailingEditFieldStyle.focusedBorderColor, font: Font.fiori(forTextStyle: .title1), foregroundColor: Color.black)) + .trailingAccessoryStyle(textFieldStyle: trailingEditFieldStyle) + .informationViewStyle(getInfoStyle(lowerValue: self.intLowerValue, upperValue: self.intUpperValue, range: 0 ... 100)).typeErased + + let singleEditableRange = 1.0 ... 70.0 + let trailingEditFieldStyle2 = FioriSliderTextFieldStyle(borderColor: Color.indigo, focusedBorderColor: Color.red) + FioriSlider( + title: AttributedString("Single Editable Range Slider (\(String(format: "%.0f", singleEditableRange.lowerBound)) - \(String(format: "%.0f", singleEditableRange.upperBound)))"), + upperValue: self.$singleEditUpperValue, + range: singleEditableRange, + description: getInfoDescription(lowerValue: singleEditableRange.lowerBound, upperValue: self.singleEditUpperValue, range: singleEditableRange, decimalPlace: 0, defaultDesc: "The single editable slider accepts integer numbers."), + onEditFieldFocusStatusChange: { + self.isEditFieldFocused2 = $0 + } + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(self.isEditFieldFocused2 ? trailingEditFieldStyle2.focusedBorderColor : Color.black) + .font(.fiori(forTextStyle: .headline, weight: .black)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .subheadline, weight: .regular)) + } + .upperThumbStyle { configuration in + configuration.upperThumb.foregroundStyle(Color.red) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.indigo) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.red) + } + .trailingAccessoryStyle(textFieldStyle: trailingEditFieldStyle2) + .informationViewStyle(getInfoStyle(lowerValue: singleEditableRange.lowerBound, upperValue: self.singleEditUpperValue, range: singleEditableRange)).typeErased + + FioriSlider( + title: AttributedString("Range Slider (0-50)"), + lowerValue: self.$disabledLowerValue, + upperValue: self.$disabledUpperValue, + range: 0.0 ... 50.0, + description: "Disabled range slider" + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(.green) + .font(.fiori(forTextStyle: .title3, weight: .regular)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .body, weight: .regular)) + } + .lowerThumbStyle { configuration in + configuration.lowerThumb.foregroundStyle(Color.yellow) + } + .upperThumbStyle { configuration in + configuration.upperThumb.foregroundStyle(Color.orange) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.gray) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.brown) + } + .leadingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle(disabledBorderColor: Color.orange)) + .trailingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle(disabledBorderColor: Color.yellow)) + .disabled(true) + + FioriSlider( + title: AttributedString("Slider (10-80)"), + value: self.$disableEditValue, + range: 10 ... 80, + description: "Disabled slider" + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(.cyan) + .font(.fiori(forTextStyle: .title2, weight: .regular)) + } + .descriptionStyle { configuration in + configuration.description.foregroundStyle(.brown) + .font(.fiori(forTextStyle: .footnote, weight: .bold)) + } + .upperThumbStyle { configuration in + configuration.upperThumb.foregroundStyle(Color.gray) + } + .activeTrackStyle { configuration in + configuration.activeTrack.foregroundStyle(Color.green) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack.foregroundStyle(Color.black) + } + .trailingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle(disabledBorderColor: .brown, font: Font.fiori(forTextStyle: .KPI))) + .disabled(true) + + FioriSlider( + title: AttributedString("Custom thumb range slider"), + lowerValue: self.$customThumbLowerValue, + upperValue: self.$customThumbUpperValue, + lowerThumb: RoundedRectangle(cornerRadius: 20), + upperThumb: Ellipse(), + thumbHalfWidth: 8 + ) + .titleStyle { configuration in + configuration.title.foregroundStyle(Color.red) + } + .lowerThumbStyle { configuration in + configuration.lowerThumb + .frame(height: 40) + .foregroundStyle(Color.red) + } + .upperThumbStyle { configuration in + configuration.upperThumb + .frame(height: 40) + .foregroundStyle(Color.green) + } + .activeTrackStyle { configuration in + configuration.activeTrack + .frame(height: 8) + .foregroundStyle(Color.yellow) + } + .inactiveTrackStyle { configuration in + configuration.inactiveTrack + .frame(height: 8) + .foregroundStyle(Color.brown) + } + } + .listStyle(.plain) + } +} + +var outOfRangeValidationFormat = "The entered value '%@' is outside the possible range (%@)" + +var rangeValueValidationFormat = "The lower value '%@' must be less than or equal to the upper value '%@'" + +func getInfoStyle(value: Double, range: ClosedRange) -> any InformationViewStyle { + (range ~= value) ? InformationViewFioriStyle.fiori : InformationViewErrorStyle.error +} + +func getInfoDescription(_ value: Double, range: ClosedRange, decimalPlace: Int = 0, defaultDesc: String = "") -> AttributedString { + AttributedString((range ~= value) ? defaultDesc : String(format: outOfRangeValidationFormat, String(format: "%.\(decimalPlace)f", value), + String(format: "%.\(decimalPlace)f-%.\(decimalPlace)f", range.lowerBound, range.upperBound))) +} + +func getInfoStyle(lowerValue: Double, upperValue: Double, range: ClosedRange) -> any InformationViewStyle { + let validation = !(range ~= lowerValue) || !(range ~= upperValue) || lowerValue > upperValue + return validation ? InformationViewErrorStyle.error : InformationViewFioriStyle.fiori +} + +func getInfoDescription(lowerValue: Double, upperValue: Double, range: ClosedRange, decimalPlace: Int = 0, defaultDesc: String = "") -> AttributedString { + let rangeString = String(format: "%.\(decimalPlace)f-%.\(decimalPlace)f", range.lowerBound, range.upperBound) + guard range ~= lowerValue else { + return AttributedString(String(format: outOfRangeValidationFormat, String(format: "%.\(decimalPlace)f", lowerValue), rangeString)) + } + guard range ~= upperValue else { + return AttributedString(String(format: outOfRangeValidationFormat, String(format: "%.\(decimalPlace)f", upperValue), rangeString)) + } + + guard lowerValue <= upperValue else { + return AttributedString(String(format: rangeValueValidationFormat, String(format: "%.\(decimalPlace)f", lowerValue), String(format: "%.\(decimalPlace)f", upperValue))) + } + + return AttributedString(defaultDesc) +} + +func getLetter(for value: Int) -> String { + guard value > 0, value <= 26 else { return "A" } + let unicodeValue = value + 64 + if let scalar = UnicodeScalar(unicodeValue) { + return String(scalar) + } + return "A" +} + +func getColor(_ value: Double) -> Color { + let clampedValue = min(max(value, 0), 1) + let red = (clampedValue == 0 || clampedValue == 1) ? clampedValue : 0.5 * sin(clampedValue * .pi * 2) + 0.5 + let green = (clampedValue == 0 || clampedValue == 1) ? clampedValue : 0.5 * sin(clampedValue * .pi * 2 + .pi / 2) + 0.5 + let blue = (clampedValue == 0 || clampedValue == 1) ? 0.233 : 0.5 * sin(clampedValue * .pi * 2 + .pi) + 0.5 + + return Color(red: red, green: green, blue: blue) +} + +struct ColorPickerView: View { + @Binding var value: Double + var step: Double + + var body: some View { + getColor(self.value) + .frame(width: 40, height: 30) + .cornerRadius(8) + } +} + +struct ColorDemoViewStyle: View { + @Binding var lowerValue: Double + @Binding var upperValue: Double + + var body: some View { + ZStack { + FioriIcon.people.customer + .font(.fiori(forTextStyle: .title1)) + .foregroundStyle(getColor(self.lowerValue)) + .background( + Circle().fill(getColor(self.upperValue)) + ) + .clipShape(Circle()) + .overlay( + Circle().stroke(getColor(self.upperValue), lineWidth: 3) + ) + } + } +} + +struct MyAccessibilityTextView: View { + @State var leadingText: String = "30" + @State var trailingText: String = "40" + + var body: some View { + HStack { + let size1 = self.calculateTextSize(isLeading: true) + TextField("", text: self.$leadingText) + .font(Font.fiori(forTextStyle: .body)) + .multilineTextAlignment(.center) + .frame(width: size1.width + 32, height: size1.height + 25) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(Color.preferredColor(.quaternaryLabel), lineWidth: 2) + ) + .accessibilitySortPriority(2) + + GeometryReader { geometry in + ZStack { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilitySortPriority(4) + + Capsule() + .fill(Color.blue) + .frame(width: geometry.size.width, height: 4) + .accessibilityHidden(true) + + Circle() + .fill(Color.red) + .frame(width: 12 * 2, height: 12 * 2) + .position( + x: geometry.size.width / 2 - 6, + y: geometry.size.height / 2 + ) + .accessibilitySortPriority(3) + } + } + .frame(height: 44) + + let size = self.calculateTextSize(isLeading: false) + TextField("", text: self.$trailingText) + .font(Font.fiori(forTextStyle: .body)) + .multilineTextAlignment(.center) + .frame(width: size.width + 32, height: size.height + 25) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(Color.preferredColor(.quaternaryLabel), lineWidth: 2) + ) + .accessibilitySortPriority(1) + } + .accessibilityElement(children: .contain) + } + + func calculateTextSize(isLeading: Bool) -> CGSize { + let target = isLeading ? self.leadingText : self.trailingText + let attributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body)] + let size = target.size(withAttributes: attributes) + return size + } +} diff --git a/Sources/FioriSwiftUICore/DataTypes/FioriSlider+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/FioriSlider+DataType.swift new file mode 100644 index 000000000..4bb5de84f --- /dev/null +++ b/Sources/FioriSwiftUICore/DataTypes/FioriSlider+DataType.swift @@ -0,0 +1,660 @@ +// +// Slider+DataType.swift +// FioriSwiftUI +// +// Created by i063052 on 2024/12/13. +// +import Combine +import FioriThemeManager +import SwiftUI + +public extension FioriSlider { + /// Create a standard slider + /// + /// A standard slider consists of a title, a selected value, and a "thumb" (an image that the user can drag along a linear "track"). + /// The track represents a continuum between two bounded values: a minimum and a maximum value. + /// By default, the formatted minimum value is displayed at the leading side of the slider, and the formatted maximum value is displayed at the trailing side of the slider. + /// + /// The title is displayed at the top left of the component, while the bound value is displayed at the top right. As the user moves the thumb, the slider continuously updates its selected value corresponding to the thumb’s position. + /// You also can customize the slider with customized value label, leading accessory view and trailing accessory view. + /// + /// - Parameters: + /// - title: The main textual title for the slider has a higher priority than `titleView` and will be displayed if it is a non-empty string. The default value is an empty string. + /// - titleView: A SwiftUI `View` that serves as an alternative title. It will be used only if `title` is an empty string. + /// - value: The slider which allows the user to select a single value within a specified range (`range`). + /// - range: The range of the slider values. The default is `0...100` + /// - step: incremental/decremental value when the thumb changes its position. The default is `1`. + /// - decimalPlaces: This property specifies the number of digits that should appear after the decimal point in the Double value for slider value. It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0`. + /// - icon: The icon image for hint text of the slider + /// - description: The hint text of the slider + /// - valueLabel: The optional string to override the default slider value label for the standard slider. + /// - valueLabelView: A SwiftUI `View` that serves as an alternative value label for the slider. This parameter will be used only if `valueLabel` is `nil`. + /// - showsValueLabel: Indicates whether the value label is to be displayed or not. The default value is `true` + /// - valueFormat: This optional format is used to format the displayed slider value in the value label view. It is also utilized for formatting the accessibility value, if provided. + /// - leadingAccessory: The customized view to override the default leading accessory view which is a text label to display the minimum value of the range + /// - showsLeadingAccessory: Indicates whether the leading accessory view is to be displayed or not. The default value is `true` + /// - leadingValueFormat: This optional format is used to format the displayed minimal value of standard slider's range in the leading accessory view, if provided. + /// - trailingAccessory: The customized view to override the default trailing accessory view which is a text label to display the maximum value of the range + /// - showsTrailingAccessory: Indicates whether the trailing accessory view is to be displayed or not. The default value is `true` + /// - trailingValueFormat: This optional format is used to format the displayed maximal value of standard slider's range in the trailing accessory view, if provided. + /// - onValueChange: An optional callback function is triggered when the user begins to drag the thumb along the standard slider's track to adjust its value. The first boolean property indicates whether the editing process has begun or ended, with `false` signifying that the editing has concluded. The second double property represents the newly adjusted slider value. + /// - thumb: the shape for thumb of the standard Slider. By default, it is circle. + /// - activeTrack: the shape for active track of the standard Slider. By default, it is capsule. + /// - inactiveTrack: the shape for inactive track of the standard Slider. By default, it is capsule. + /// - thumbHalfWidth: the half width of the thumb of the standard slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + /// + init(title: AttributedString = "", + @ViewBuilder titleView: () -> any View = { EmptyView() }, + value: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + icon: Image? = nil, + description: AttributedString? = nil, + valueLabel: AttributedString? = nil, + @ViewBuilder valueLabelView: () -> any View = { EmptyView() }, + showsValueLabel: Bool = true, + valueFormat: String? = nil, + @ViewBuilder leadingAccessory: () -> any View = { EmptyView() }, + showsLeadingAccessory: Bool = true, + leadingValueFormat: String? = nil, + @ViewBuilder trailingAccessory: () -> any View = { EmptyView() }, + showsTrailingAccessory: Bool = true, + trailingValueFormat: String? = nil, + onValueChange: ((Bool, Double) -> Void)? = nil, + wrapperBuiltInSlider: Bool = false, + thumb: any Shape = Circle(), + activeTrack: any Shape = Capsule(), + inactiveTrack: any Shape = Capsule(), + thumbHalfWidth: CGFloat = 14) + { + var theTitle = titleView + if !title.characters.isEmpty { + theTitle = { Text(title) } + } + var theValueLabel = valueLabelView + if let label = valueLabel { + theValueLabel = { Text(label) } + } + var theDescription: () -> any View = { EmptyView() } + if let desc = description { + theDescription = { Text(desc) } + } + self.init(title: theTitle, + valueLabel: theValueLabel, + lowerThumb: { Circle() }, + upperThumb: { thumb }, + activeTrack: { activeTrack }, + inactiveTrack: { inactiveTrack }, + lowerValue: Binding(get: { range.lowerBound }, set: { _ in }), + upperValue: value, + range: range, + step: step, + decimalPlaces: decimalPlaces, + thumbHalfWidth: thumbHalfWidth, + showsLowerThumb: false, + showsUpperThumb: true, + icon: { icon }, + description: theDescription, + leadingAccessory: leadingAccessory, + trailingAccessory: trailingAccessory, + isRangeSlider: false, + valueFormat: valueFormat, + leadingValueFormat: leadingValueFormat, + trailingValueFormat: trailingValueFormat, + showsValueLabel: showsValueLabel, + showsLeadingAccessory: showsLeadingAccessory, + showsTrailingAccessory: showsTrailingAccessory, + onValueChange: onValueChange) + } + + /// Create a range slider + /// + /// A range slider consists of a title, a bound lower value, a bound upper value, and two "thumbs" (images that users can drag along a linear "track"). + /// The track represents a continuum between two extremes: a minimum and a maximum value. + /// By default, the formatted lower value is displayed in a text field at the leading end of the slider, and the formatted upper value is displayed in a text field at the trailing end of the slider. + /// The title is displayed at the top left of the component. As users edit the lower or upper value in the text fields or move the thumbs, the slider continuously updates the bound values to reflect the thumbs’ positions. + /// You can also customize the slider with a customized value label, leading accessory view, and trailing accessory view. + /// + /// - Parameters: + /// - title: The main textual title for the slider has a higher priority than `titleView` and will be displayed if it is a non-empty string. The default value is an empty string. + /// - titleView: A SwiftUI `View` that serves as an alternative title. It will be used only if `title` is an empty string. + /// - upperValue: The upper value of range slider. + /// - range: The range of the slider values. The default is `0...100` + /// - step: incremental/decremental value when the thumb changes its position. The default is `1`. + /// - decimalPlaces: This property specifies the number of digits that should appear after the decimal point in the Double value for slider value. It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0`. + /// - icon: The icon image for hint text of the slider + /// - description: The hint text of the slider + /// - valueLabel: The optional customized string for value label which display at the top right of the slider + /// - valueLabelView: A SwiftUI `View` that serves as an alternative value label for the slider. This parameter will be used only if `valueLabel` is `nil`. + /// - rangeFormat: The optional formats are used to format the lower and upper bound values of the range. They are utilized for formatting the accessibility values when you customize the range slider with your own leading and trailing accessory views, if provided. + /// - leadingAccessory: The customized view to override the default leading accessory view which is a text field to display the lower value + /// - showsLeadingAccessory: Indicates whether the leading accessory view is to be displayed or not. The default value is `true` + /// - leadingValueFormat: This optional format is used to format the displayed lower value of range slider in the leading accessory view, if provided. + /// - trailingAccessory: The customized view to override the default trailing accessory view which is a text field to display the upper value + /// - showsTrailingAccessory: Indicates whether the trailing accessory view is to be displayed or not. The default value is `true` + /// - trailingValueFormat: This optional format is used to format the displayed upper value of range slider in the leading accessory view, if provided. + /// - onRangeValueChange: An optional callback function that is triggered when the user begins to drag either the lower or upper thumb along the range slider's track to adjust the slider's values. The first boolean parameter indicates whether the editing has begun or ended, with `false` signifying that the editing has ended. The second parameter is a double that represents the updated lower value, while the third parameter (also a double) represents the updated upper value. + /// - lowerThumb: the shape for lower thumb of the range Slider. By default, it is circle. + /// - upperThumb: the shape for upper thumb of the range Slider. By default, it is circle. + /// - activeTrack: the shape for active track of the range Slider. By default, it is capsule. + /// - inactiveTrack: the shape for inactive track of the range Slider. By default, it is capsule. + /// - thumbHalfWidth: the half width of the thumb of the range slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + /// - onEditFieldFocusStatusChange: An optional callback function is triggered when the focus state of a text field, which serves as a leading or trailing accessory for an editable slider, changes. The boolean parameter of the callback indicates the focus state of the text field. This can be useful for obtaining the focus state when customizing the editable slider. + /// + init(title: AttributedString = "", + @ViewBuilder titleView: () -> any View = { EmptyView() }, + lowerValue: Binding, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + icon: Image? = nil, + description: AttributedString? = nil, + valueLabel: AttributedString? = nil, + @ViewBuilder valueLabelView: () -> any View = { EmptyView() }, + rangeFormat: (String, String)? = nil, + @ViewBuilder leadingAccessory: () -> any View = { EmptyView() }, + showsLeadingAccessory: Bool = true, + leadingValueFormat: String? = nil, + @ViewBuilder trailingAccessory: () -> any View = { EmptyView() }, + showsTrailingAccessory: Bool = true, + trailingValueFormat: String? = nil, + onRangeValueChange: ((Bool, Double, Double) -> Void)? = nil, + lowerThumb: any Shape = Circle(), + upperThumb: any Shape = Circle(), + activeTrack: any Shape = Capsule(), + inactiveTrack: any Shape = Capsule(), + thumbHalfWidth: CGFloat = 14, + onEditFieldFocusStatusChange: ((Bool) -> Void)? = nil) + { + var theTitle = titleView + if !title.characters.isEmpty { + theTitle = { Text(title) } + } + var theValueLabel = valueLabelView + if let label = valueLabel { + theValueLabel = { Text(label) } + } + var theDescription: () -> any View = { EmptyView() } + if let desc = description { + theDescription = { Text(desc) } + } + self.init(title: theTitle, + valueLabel: theValueLabel, + lowerThumb: { lowerThumb }, + upperThumb: { upperThumb }, + activeTrack: { activeTrack }, + inactiveTrack: { inactiveTrack }, + lowerValue: lowerValue, + upperValue: upperValue, + range: range, + step: step, + decimalPlaces: decimalPlaces, + thumbHalfWidth: thumbHalfWidth, + showsLowerThumb: showsLeadingAccessory, + showsUpperThumb: showsTrailingAccessory, + onRangeValueChange: onRangeValueChange, + icon: { icon }, + description: theDescription, + leadingAccessory: leadingAccessory, + trailingAccessory: trailingAccessory, + isRangeSlider: true, + rangeFormat: rangeFormat, + leadingValueFormat: leadingValueFormat, + trailingValueFormat: trailingValueFormat, + showsValueLabel: true, // Always set it as true and only show custom valueLabel, if provided. + showsLeadingAccessory: showsLeadingAccessory, + showsTrailingAccessory: showsTrailingAccessory, + onEditFieldFocusStatusChange: onEditFieldFocusStatusChange) + } + + /// Create a single editable range slider + /// + /// A range slider consists of a title, a bound upper value, and a "thumb" (images that users can drag along a linear "track"). + /// The track represents a continuum between two extremes: a minimum and a maximum value. + /// By default, the formatted upper value is displayed in a text field at the trailing end of the slider. + /// The title is displayed at the top left of the component. As users edit the upper value in the text fields or move the thumb, the slider continuously updates the bound values to reflect the thumbs’ positions. + /// + /// - Parameters: + /// - title: The main textual title for the slider has a higher priority than `titleView` and will be displayed if it is a non-empty string. The default value is an empty string. + /// - titleView: A SwiftUI `View` that serves as an alternative title. It will be used only if `title` is an empty string. + /// - upperValue: The value of single editable slider. + /// - range: The range of the slider values. The default is `0...100` + /// - step: incremental/decremental value when the thumb changes its position. The default is `1`. + /// - decimalPlaces: This property specifies the number of digits that should appear after the decimal point in the Double value for slider value. It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0`. + /// - icon: The icon image for hint text of the slider + /// - description: The hint text of the slider + /// - valueLabel: The optional customized value for value label which display at the top right of the slider + /// - valueLabelView: A SwiftUI `View` that serves as an alternative value label for the slider. This parameter will be used only if `valueLabel` is `nil`. + /// - rangeFormat: The optional formats are used to format the lower and upper bound values of the range. They are utilized for formatting the accessibility values when you customize the range slider with your own leading and trailing accessory views, if provided. + /// - - onValueChange: An optional callback function is triggered when the user drag the thumb to change bound value or finish edit in text field. The first boolean property indicates whether the editing process has begun or ended, with `false` signifying that the editing has concluded. The second double property represents the newly adjusted slider value. + /// - upperThumb: the shape for upper thumb of the range Slider. By default, it is circle. + /// - activeTrack: the shape for active track of the range Slider. By default, it is capsule. + /// - inactiveTrack: the shape for inactive track of the range Slider. By default, it is capsule. + /// - thumbHalfWidth: the half width of the thumb of the range slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + /// - onEditFieldFocusStatusChange: An optional callback function is triggered when the focus state of a text field, which serves as trailing accessory for an editable slider, changes. The boolean parameter of the callback indicates the focus state of the text field. This can be useful for obtaining the focus state when customizing the editable slider. + /// + init(title: AttributedString = "", + @ViewBuilder titleView: () -> any View = { EmptyView() }, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + icon: Image? = nil, + description: AttributedString? = nil, + valueLabel: AttributedString? = nil, + @ViewBuilder valueLabelView: () -> any View = { EmptyView() }, + rangeFormat: (String, String)? = nil, + onValueChange: ((Bool, Double) -> Void)? = nil, + upperThumb: any Shape = Circle(), + activeTrack: any Shape = Capsule(), + inactiveTrack: any Shape = Capsule(), + thumbHalfWidth: CGFloat = 14, + onEditFieldFocusStatusChange: ((Bool) -> Void)? = nil) + { + var theTitle = titleView + if !title.characters.isEmpty { + theTitle = { Text(title) } + } + var theValueLabel = valueLabelView + if let label = valueLabel { + theValueLabel = { Text(label) } + } + var theDescription: () -> any View = { EmptyView() } + if let desc = description { + theDescription = { Text(desc) } + } + self.init(title: theTitle, + valueLabel: theValueLabel, + lowerThumb: { upperThumb }, + upperThumb: { upperThumb }, + activeTrack: { activeTrack }, + inactiveTrack: { inactiveTrack }, + lowerValue: Binding(get: { range.lowerBound }, set: { _ in }), + upperValue: upperValue, + range: range, + step: step, + decimalPlaces: decimalPlaces, + thumbHalfWidth: thumbHalfWidth, + showsLowerThumb: false, + showsUpperThumb: true, + icon: { icon }, + description: theDescription, + isRangeSlider: true, + rangeFormat: rangeFormat, + showsValueLabel: true, // Always set it as true and only show custom valueLabel + showsLeadingAccessory: false, + showsTrailingAccessory: true, + onValueChange: onValueChange, + onEditFieldFocusStatusChange: onEditFieldFocusStatusChange) + } +} + +public extension View { + /// Clear the focus state of the text field in FioirSlider + func clearFioriSliderEditableAccessoryFocusOnTap() -> some View { + self.modifier(LostFocusOnTap()) + } + + /// Customize the appearance of the leading text field + /// + /// - Parameters: + /// - textFieldStyle: The instance of `FioriSliderTextFieldStyle` for leading text field + /// + func leadingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle) -> some View { + self.modifier(FioriTextFieldStylesModifier(leadingStyle: textFieldStyle)) + } + + /// Customize the appearance of the trailing text field + /// + /// - Parameters: + /// - textFieldStyle: The instance of `FioriSliderTextFieldStyle` for trailing text field + /// + func trailingAccessoryStyle(textFieldStyle: FioriSliderTextFieldStyle) -> some View { + self.modifier(FioriTextFieldStylesModifier(trailingStyle: textFieldStyle)) + } +} + +/// The `FioriSliderTextFieldStyle` structure is used to customize the appearance of the text field in a Fiori Slider. +/// It allows for the configuration of various properties such as border colors, widths, corner radius, font, and foreground colors. +/// Consumers can create their own instances of `FioriSliderTextFieldStyle` to apply custom styles to the slider text field. +public struct FioriSliderTextFieldStyle { + /// Specifies the border color of the text field when it is not focused. + public var borderColor: Color + + /// Specifies the border color of the text field when it is focused. + public var focusedBorderColor: Color + + /// Specifies the border color of the text field when it is disabled. + public var disabledBorderColor: Color + + /// Specifies the width of the border of the text field when it is not focused. + public var borderWidth: CGFloat + + /// Specifies the width of the border of the text field when it is focused. + public var focusedBorderWidth: CGFloat + + /// Specifies the corner radius of the text field. + public var cornerRadius: CGFloat + + /// Specifies the font used for the text inside the text field. + public var font: Font + + /// Specifies the color of the text inside the text field. + public var foregroundColor: Color + + /// Specifies the color of the text inside the text field when it is disabled. + public var disabledForegroundColor: Color + + /// Creates a custom style for the slider text field by specifying various properties. If a property is not provided, a default value will be used. + /// + /// - Parameters: + /// - borderColor: Optional `Color` to specify the border color when the text field is not focused. If `nil`, the default border color will be used. + /// - focusedBorderColor: Optional `Color` to specify the border color when the text field is focused. If `nil`, the default focused border color will be used. + /// - disabledBorderColor: Optional `Color` to specify the border color when the text field is disabled. If `nil`, the default disabled border color will be used + /// - borderWidth: Optional `CGFloat` to specify the border width when the text field is not focused. If `nil`, the default border width will be used. + /// - focusedBorderWidth: Optional `CGFloat` to specify the border width when the text field is focused. If `nil`, the default focused border width will be used + /// - cornerRadius: Optional `CGFloat` to specify the corner radius of the text field. If `nil`, the default corner radius will be used + /// - font: Optional `Font` to specify the font used for the text inside the text field. If `nil`, the default font will be used + /// - foregroundColor: Optional `Color` to specify the color of the text inside the text field. If `nil`, the default foreground color will be used + /// - disabledForegroundColor: Optional `Color` to specify the color of the text inside the text field when it is disabled. If `nil`, the default disabled foreground will be used + /// + public init(borderColor: Color? = nil, focusedBorderColor: Color? = nil, disabledBorderColor: Color? = nil, borderWidth: CGFloat? = nil, focusedBorderWidth: CGFloat? = nil, cornerRadius: CGFloat? = nil, font: Font? = nil, foregroundColor: Color? = nil, disabledForegroundColor: Color? = nil) { + self.borderColor = borderColor ?? FioriSliderTextFieldStyle.leading.borderColor + self.focusedBorderColor = focusedBorderColor ?? FioriSliderTextFieldStyle.leading.focusedBorderColor + self.disabledBorderColor = disabledBorderColor ?? FioriSliderTextFieldStyle.leading.disabledBorderColor + self.borderWidth = borderWidth ?? FioriSliderTextFieldStyle.leading.borderWidth + self.focusedBorderWidth = focusedBorderWidth ?? FioriSliderTextFieldStyle.leading.focusedBorderWidth + self.cornerRadius = cornerRadius ?? FioriSliderTextFieldStyle.leading.cornerRadius + self.font = font ?? FioriSliderTextFieldStyle.leading.font + self.foregroundColor = foregroundColor ?? FioriSliderTextFieldStyle.leading.foregroundColor + self.disabledForegroundColor = disabledForegroundColor ?? FioriSliderTextFieldStyle.leading.disabledForegroundColor + } + + static let leading = FioriSliderTextFieldStyle(borderColor: Color(.opaqueSeparator), focusedBorderColor: Color.preferredColor(.tintColor), disabledBorderColor: Color.preferredColor(.quaternaryLabel), borderWidth: 1, focusedBorderWidth: 2, cornerRadius: 10, font: Font.fiori(forTextStyle: .body), foregroundColor: Color.preferredColor(.primaryLabel), disabledForegroundColor: Color.preferredColor(.quaternaryLabel)) + + static let trailing = FioriSliderTextFieldStyle(borderColor: Color(.opaqueSeparator), focusedBorderColor: Color.preferredColor(.tintColor), disabledBorderColor: Color.preferredColor(.quaternaryLabel), borderWidth: 1, focusedBorderWidth: 2, cornerRadius: 10, font: Font.fiori(forTextStyle: .body), foregroundColor: Color.preferredColor(.primaryLabel), disabledForegroundColor: Color.preferredColor(.quaternaryLabel)) +} + +extension ValueLabel { + init(_ configuration: ValueLabelConfiguration, fioriSliderConfiguration: FioriSliderConfiguration) { + if !fioriSliderConfiguration.showsValueLabel { + self.init(valueLabel: { EmptyView() }) + } else if configuration.valueLabel.isEmpty, !fioriSliderConfiguration.isRangeSlider { // By default, display slider value for standard slider if there is no customized value label + self.init(valueLabel: { Text(String(format: fioriSliderConfiguration.valueFormat ?? "%.\(fioriSliderConfiguration.decimalPlaces)f", fioriSliderConfiguration.upperValue)) }) + } else { + self.init(configuration) + } + } +} + +extension LeadingAccessory { + init(configuration: LeadingAccessoryConfiguration, fioriSliderConfiguration: FioriSliderConfiguration) { + if !fioriSliderConfiguration.showsLeadingAccessory { + self.init(leadingAccessory: { EmptyView() }) + } else if !configuration.leadingAccessory.isEmpty { // display customized leading view + self.init(configuration) + } else if fioriSliderConfiguration.isRangeSlider { // display text field for range slider by default + self.init(leadingAccessory: { NumbersOnlyTextField(configuration: fioriSliderConfiguration, isLeadingAccessory: true) }) + } else { // display lower bound of range for standard slider + self.init(leadingAccessory: { Text(String(format: fioriSliderConfiguration.leadingValueFormat ?? "%.\(fioriSliderConfiguration.decimalPlaces)f", fioriSliderConfiguration.range.lowerBound)) }) + } + } +} + +extension TrailingAccessory { + init(configuration: TrailingAccessoryConfiguration, fioriSliderConfiguration: FioriSliderConfiguration) { + if !fioriSliderConfiguration.showsTrailingAccessory { + self.init(trailingAccessory: { EmptyView() }) + } else if !configuration.trailingAccessory.isEmpty { // display customized trailing view + self.init(configuration) + } else if fioriSliderConfiguration.isRangeSlider { // display text field for range slider + self.init(trailingAccessory: { NumbersOnlyTextField(configuration: fioriSliderConfiguration, isLeadingAccessory: false) }) + } else { // display upper bound of range for standard slider + self.init(trailingAccessory: { Text(String(format: fioriSliderConfiguration.trailingValueFormat ?? "%.\(fioriSliderConfiguration.decimalPlaces)f", fioriSliderConfiguration.range.upperBound)) }) + } + } +} + +struct LostFocusOnTap: ViewModifier { + func body(content: Content) -> some View { + content + .contentShape(Rectangle()) // Make the entire view tappable + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + } +} + +struct NumbersOnlyTextField: View { + let configuration: FioriSliderConfiguration + var isLeadingAccessory: Bool = false + @Environment(\.isEnabled) private var isEnabled + @Environment(\.sliderRowSize) private var sliderRowSize + @Environment(\.leadingTextFieldStyle) private var leadingTextFieldStyle + @Environment(\.trailingTextFieldStyle) private var trailingTextFieldStyle + @Environment(\.roundValueFormat) private var roundValueFormat + @EnvironmentObject private var stateObject: SliderStateObject + @EnvironmentObject private var modelObject: SliderModelObject + @FocusState private var isTextFieldFocused: Bool + + var body: some View { + let textSize = self.estimateTextSize() // estimate the size of the displayed text and set it as the text field's size to make sure the size can be changed according the editing value. + let borderColor = self.isLeadingAccessory ? self.leadingTextFieldStyle.borderColor : self.trailingTextFieldStyle.borderColor + let focusedBorderColor = self.isLeadingAccessory ? self.leadingTextFieldStyle.focusedBorderColor : self.trailingTextFieldStyle.focusedBorderColor + let disabledBorderColor = self.isLeadingAccessory ? self.leadingTextFieldStyle.disabledBorderColor : self.trailingTextFieldStyle.disabledBorderColor + let borderWidth = self.isLeadingAccessory ? self.leadingTextFieldStyle.borderWidth : self.trailingTextFieldStyle.borderWidth + let focusedBorderWidth = self.isLeadingAccessory ? self.leadingTextFieldStyle.focusedBorderWidth : self.trailingTextFieldStyle.focusedBorderWidth + TextField( + "", + text: self.isLeadingAccessory ? self.$modelObject.leadingText : self.$modelObject.trailingText + ) + .numbersOnly(self.isLeadingAccessory ? self.$modelObject.leadingText : self.$modelObject.trailingText, decimalPlace: self.configuration.decimalPlaces, onValueChange: { value in + self.isLeadingAccessory ? (self.stateObject.editingLeadingValue = value) : (self.stateObject.editingTrailingValue = value) + // To store the current editing value in state object and it will be used when editing done. + }) // To use $modelObject.leadingText or trailingText and let NumbersOnlyViewModifier can parse and check the editing value. + .disableAutocorrection(true) + .autocapitalization(.none) + .multilineTextAlignment(.center) + .frame(width: textSize.width, height: textSize.height) // Don't use minWidth, maxWidth here to avoid the text field re-render un-expected and let the editing value refreshed. + .cornerRadius(self.isLeadingAccessory ? self.leadingTextFieldStyle.cornerRadius : self.trailingTextFieldStyle.cornerRadius) + .focused(self.$isTextFieldFocused) + .onChange(of: self.isTextFieldFocused) { + self.stateObject.isFocused = self.isTextFieldFocused + if !self.isTextFieldFocused { // Update the slider value and notify consumer when text field lost focus + if let editingValue = isLeadingAccessory ? self.stateObject.editingLeadingValue : self.stateObject.editingTrailingValue, let newValue = Double(editingValue) { + self.isLeadingAccessory ? (self.stateObject.editingLeadingValue = nil) : (self.stateObject.editingTrailingValue = nil) // always clear the temporarily stored editing value + + let valueChanged = self.isLeadingAccessory && newValue != self.configuration.lowerValue || !self.isLeadingAccessory && newValue != self.configuration.upperValue + + guard valueChanged else { // Make sure to notify consumer only when the value was changed + return + } + + self.isLeadingAccessory ? (self.configuration.lowerValue = newValue) : (self.configuration.upperValue = newValue) + if let onRangeValueChange = configuration.onRangeValueChange, configuration.showsLowerThumb, configuration.showsUpperThumb { + onRangeValueChange(false, self.configuration.lowerValue, self.configuration.upperValue) + } else if let onValueChange = configuration.onValueChange, configuration.showsLowerThumb != configuration.showsUpperThumb { + onValueChange(false, newValue) + } + } + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + if let onChange = self.configuration.onEditFieldFocusStatusChange { + onChange(self.isTextFieldFocused) + } + } + .overlay( + RoundedRectangle(cornerRadius: self.isLeadingAccessory ? self.leadingTextFieldStyle.cornerRadius : self.trailingTextFieldStyle.cornerRadius) + .strokeBorder(self.isEnabled ? ((self.isTextFieldFocused && self.isLeadingAccessory || self.isTextFieldFocused && !self.isLeadingAccessory) ? focusedBorderColor : borderColor) : disabledBorderColor, lineWidth: (self.isTextFieldFocused && self.isLeadingAccessory || self.isTextFieldFocused && !self.isLeadingAccessory) ? focusedBorderWidth : borderWidth) + ) + .font(self.isLeadingAccessory ? self.leadingTextFieldStyle.font : self.trailingTextFieldStyle.font) + .foregroundStyle(self.isEnabled ? (self.isLeadingAccessory ? self.leadingTextFieldStyle.foregroundColor : self.trailingTextFieldStyle.foregroundColor) : (self.isLeadingAccessory ? self.leadingTextFieldStyle.disabledForegroundColor : self.trailingTextFieldStyle.disabledForegroundColor)) + } +} + +extension TextField { + func numbersOnly(_ text: Binding, decimalPlace: Int = 0, onValueChange: ((String) -> Void)? = nil) -> some View { + self.modifier(NumbersOnlyViewModifier(text: text, decimalPlace: decimalPlace, onValueChange: onValueChange)) + } +} + +extension NumbersOnlyTextField { + func estimateTextSize() -> CGSize { // Just estimate the size for display text. + let text = self.isLeadingAccessory ? self.modelObject.leadingText : self.modelObject.trailingText + let characterSize = self.midSizeNumberCharacters(withFont: self.isLeadingAccessory ? self.leadingTextFieldStyle.font : self.trailingTextFieldStyle.font) + let count = self.configuration.decimalPlaces == 0 ? text.count : (text.count - 1) + let height = max(characterSize.height + 16, 44) + let width = min(max(characterSize.width * CGFloat(count) + 32, 51), self.sliderRowSize.width / 3) // The min width was 51 and max width was 1/3 of row width + return CGSize(width: width, height: height) + } + + func midSizeNumberCharacters(withFont: Font) -> CGSize { // estimate the middle size for possible characters + var sizes: [CGSize] = [] + let attributes = [NSAttributedString.Key.font: self.fontFrom(font: withFont)] + for digit in "0123456789" { + let size = String(digit).size(withAttributes: attributes) + sizes.append(size) + } + sizes.sort { $0.width < $1.width } // Sort sizes by width (or any other criteria, such as height if needed) + if sizes.count % 2 == 1 { // Return the middle element if there's an odd number of sizes + return sizes[sizes.count / 2] + } else { // Return the average of the two middle elements if there's an even number of sizes + let midIndex = sizes.count / 2 + let size1 = sizes[midIndex - 1] + let size2 = sizes[midIndex] + let averageWidth = (size1.width + size2.width) / 2 + let averageHeight = (size1.height + size2.height) / 2 + return CGSize(width: averageWidth, height: averageHeight) + } + } + + func fontFrom(font: Font) -> UIFont { + switch font { + case .largeTitle: + return UIFont.preferredFont(forTextStyle: .largeTitle) + case .title: + return UIFont.preferredFont(forTextStyle: .title1) + case .title2: + return UIFont.preferredFont(forTextStyle: .title2) + case .title3: + return UIFont.preferredFont(forTextStyle: .title3) + case .headline: + return UIFont.preferredFont(forTextStyle: .headline) + case .subheadline: + return UIFont.preferredFont(forTextStyle: .subheadline) + case .body: + return UIFont.preferredFont(forTextStyle: .body) + case .callout: + return UIFont.preferredFont(forTextStyle: .callout) + case .footnote: + return UIFont.preferredFont(forTextStyle: .footnote) + case .caption: + return UIFont.preferredFont(forTextStyle: .caption1) + case .caption2: + return UIFont.preferredFont(forTextStyle: .caption2) + default: + return UIFont.preferredFont(forTextStyle: .body) + } + } +} + +struct NumbersOnlyViewModifier: ViewModifier { + @EnvironmentObject private var stateObject: SliderStateObject + @Binding var text: String + var decimalPlace: Int + var onValueChange: ((String) -> Void)? + + func body(content: Content) -> some View { + content.keyboardType(self.decimalPlace > 0 ? .decimalPad : .numberPad) + .onReceive(Just(self.text)) { newValue in + var numbers = "0123456789" + let decimalSeparator = Locale.current.decimalSeparator ?? "." + if self.decimalPlace > 0 { + numbers += decimalSeparator + } + if newValue.components(separatedBy: decimalSeparator).count - 1 > 1 { // Always make sure the editing string value only has one decimalSeparator + let filtered = newValue + self.text = String(filtered.dropLast()) + } else { + var filtered = newValue.filter { numbers.contains($0) } + let components = filtered.split(separator: decimalSeparator) + if components.count == 2, components[1].count > self.decimalPlace { // make sure the edited values match the decimal places exactly. + filtered = String(filtered.prefix(components[0].count + 1 + self.decimalPlace)) + } + if filtered != newValue { + self.text = filtered + } + } + if let onValueChange = self.onValueChange { + onValueChange(self.text) + } + } + } +} + +struct FioriTextFieldStylesModifier: ViewModifier { // To change the appearance of the Range Slider text field + var leadingStyle: FioriSliderTextFieldStyle? + var trailingStyle: FioriSliderTextFieldStyle? + + func body(content: Content) -> some View { + guard let leadingStyle = self.leadingStyle, let trailingStyle = self.trailingStyle else { + if let style = self.leadingStyle { + return AnyView(content + .environment(\.leadingTextFieldStyle, style)) + } else if let style = self.trailingStyle { + return AnyView(content + .environment(\.trailingTextFieldStyle, style)) + } else { + return AnyView(content) + } + } + + return AnyView(content.environment(\.leadingTextFieldStyle, leadingStyle) + .environment(\.trailingTextFieldStyle, trailingStyle)) + } +} + +struct SliderLeadingTextFieldStyleKey: EnvironmentKey { + static let defaultValue: FioriSliderTextFieldStyle = .leading +} + +struct SliderTrailingTextFieldStyleKey: EnvironmentKey { + static let defaultValue: FioriSliderTextFieldStyle = .trailing +} + +struct FioriSliderValueFormatKey: EnvironmentKey { + static let defaultValue = "%.0f" +} + +struct FioriSliderSizeKey: EnvironmentKey { + static let defaultValue: CGSize = .zero +} + +extension EnvironmentValues { + var roundValueFormat: String { + get { self[FioriSliderValueFormatKey.self] } + set { self[FioriSliderValueFormatKey.self] = newValue } + } + + var sliderRowSize: CGSize { + get { self[FioriSliderSizeKey.self] } + set { self[FioriSliderSizeKey.self] = newValue } + } + + var sliderSize: CGSize { + get { self[FioriSliderSizeKey.self] } + set { self[FioriSliderSizeKey.self] = newValue } + } + + var leadingTextFieldStyle: FioriSliderTextFieldStyle { + get { self[SliderLeadingTextFieldStyleKey.self] } + set { self[SliderLeadingTextFieldStyleKey.self] = newValue } + } + + var trailingTextFieldStyle: FioriSliderTextFieldStyle { + get { self[SliderTrailingTextFieldStyleKey.self] } + set { self[SliderTrailingTextFieldStyleKey.self] = newValue } + } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift index 3f79f960a..58e18310e 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift @@ -467,3 +467,43 @@ protocol _ProgressComponent { // sourcery: defaultValue = "ProgressView()" var progress: ProgressView { get } } + +// sourcery: BaseComponent +protocol _LowerThumbComponent { + // sourcery: @ViewBuilder + // sourcery: defaultValue = "Circle()" + var lowerThumb: any Shape { get } +} + +// sourcery: BaseComponent +protocol _UpperThumbComponent { + // sourcery: @ViewBuilder + // sourcery: defaultValue = "Circle()" + var upperThumb: any Shape { get } +} + +// sourcery: BaseComponent +protocol _ActiveTrackComponent { + // sourcery: @ViewBuilder + // sourcery: defaultValue = "Capsule()" + var activeTrack: any Shape { get } +} + +// sourcery: BaseComponent +protocol _InactiveTrackComponent { + // sourcery: @ViewBuilder + // sourcery: defaultValue = "Capsule()" + var inactiveTrack: any Shape { get } +} + +// sourcery: BaseComponent +protocol _LeadingAccessoryComponent { + @ViewBuilder + var leadingAccessory: (() -> any View)? { get } +} + +// sourcery: BaseComponent +protocol _TrailingAccessoryComponent { + @ViewBuilder + var trailingAccessory: (() -> any View)? { get } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index ed22ae321..a0b352ab9 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -741,3 +741,225 @@ protocol _ActivityItemComponent: _IconComponent, _SubtitleComponent { // sourcery: defaultValue = .vertical var layout: ActivityItemLayout { get } } + +// sourcery: CompositeComponent +protocol _RangeSliderControlComponent: _LowerThumbComponent, _UpperThumbComponent, _ActiveTrackComponent, _InactiveTrackComponent { + // sourcery: @Binding + /// The lower value of range slider. + var lowerValue: Double { get } + + // sourcery: @Binding + /// The upper value of range slider + var upperValue: Double { get } + + // sourcery: defaultValue = 0...100 + /// The range of the slider values. The default is `0...100`. + var range: ClosedRange { get } + + // sourcery: defaultValue = 1 + /// The incremental/decremental value when the thumb changes its position. The default is `1`. + var step: Double { get } + + // sourcery: defaultValue = 0 + /// This property specifies the number of digits that should appear after the decimal point in the Double value for slider value or lower/upper value for range slider . It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0` + var decimalPlaces: Int { get } + + // sourcery: defaultValue = 14 + /// The half-width of the thumb. This value only takes effect for a range slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + var thumbHalfWidth: CGFloat { get } + + // sourcery: defaultValue = "true" + /// Indicates whether the lower thumb is to be displayed or not. The default value is `true` + var showsLowerThumb: Bool { get } + + // sourcery: defaultValue = "true" + /// Indicates whether the upper thumb is to be displayed or not. The default value is `true` + var showsUpperThumb: Bool { get } + + /// An optional callback function that is triggered when the user begins to drag either the lower or upper thumb along the range slider's track or edit the value in text field to adjust the slider's values. The first boolean parameter indicates whether the editing has begun or ended, with `false` signifying that the editing has ended. The second parameter is a double that represents the updated lower value, while the third parameter (also a double) represents the updated upper value. + var onRangeValueChange: ((Bool, Double, Double) -> Void)? { get } +} + +/// The `FioriSlider` is a SwiftUI component that provides both a standard slider and a range slider. +/// The standard slider allows users to select a single value, while the range slider allows users to select a range of values with two thumbs. +/// +/// ## Usage +/// +/// ### Standard Slider: +/// +/// A standard slider consists of a title, a bound value, and a "thumb" (an image that users can drag along a linear "track". +/// The track represents a continuum between two extremes: a minimum and a maximum value. +/// By default, the formatted minimum value is displayed at the leading end of the slider, and the formatted maximum value is displayed at the trailing end of the slider. +/// +/// The title is displayed at the top left of the component, while the bound value is displayed at the top right. As users move the thumb, the slider continuously updates its bound value to reflect the thumb’s position. +/// +/// The following example illustrates a standard slider bound to the value `speed`. The slider uses the default range of `0` to `100`, with a default step of `1`. +/// The minimum value of the range is displayed as the leading accessory view label, while the maximum value is shown as the trailing accessory view label. +/// As the slider updates the `speed` value, the updated value is displayed in a label at the top right of the slider. +/// +/// ```swift +/// @State private var speed: Double = 20 +/// +/// FioriSlider( +/// title: "Speed Limit", +/// value: $speed, +/// description: "Simple standard slider" +/// ) +/// ``` +/// +/// You can also use the `range` parameter to specify the value range of the slider. +/// The `step` parameter allows you to define incremental steps along the slider's path. +/// The `decimalPlaces` parameter can be used to manage the decimal places of the slider's value. +/// To format the bound value for display, use the `valueFormat` parameter. +/// The `leadingValueFormat` parameter customizes the leading value label, which displays the minimum value of the range. +/// Similarly, the `trailingValueFormat` parameter customizes the trailing value label, which displays the maximum value of the range. +/// Additionally, you can use the `showsValueLabel`, `showsLeadingAccessory`, and `showsTrailingAccessory` parameters to control the display of the related labels. +/// The `onValueChange` closure passed to the slider provides callbacks when the user drags the slider. +/// +/// ```swift +/// @State private var speed: Double = 20 +/// +/// FioriSlider( +/// title: "Speed Limit", +/// value: $speed, +/// range: 10...200, +/// step: 2.5, +/// decimalPlaces: 1, +/// description: "Simple standard slider", +/// valueLabelFormat: "%.1f KM", +/// leadingLabelFormat: "%.1f KM", +/// trailingLabelFormat: "%.1f KM", +/// onValueChange: { isEditing, newSpeed in +/// if !isEditing { +/// print("The speed was changed to: " + String(format: "%.1f", value)) +/// } +/// } +/// ) +/// ``` +/// The example above illustrates a standard slider with a range of `10` to `200` and a step increment of `2.5`. +/// Therefore, the slider's increments would be `10`, `12.5`, `15`, and so on. +/// At the same time, the minimum value of the range is formatted and displayed as `10.0 KM`. +/// Similarly, the maximum value of the range is formatted and displayed as `200.0 KM`. +/// The updated value can be received within the `onValueChange` closure callback when the user drags the slider. +/// +/// The slider also uses the `step` to increase or decrease the value when a +/// VoiceOver user adjusts the slider with voice commands. +/// +/// The `FioriSlider` supports a modifier called `accessibilityAdjustments`, which allows you to adjust the accessibility settings for a standard slider according to the Fiori Slider guidelines. +/// +/// ### Range Slider: +/// +/// A range slider consists of a title, a bound lower value, a bound upper value, and two "thumbs" (images that users can drag along a linear "track"). +/// The track represents a continuum between two extremes: a minimum and a maximum value. +/// By default, the formatted lower value is displayed in a text field at the leading end of the slider, and the formatted upper value is displayed in a text field at the trailing end of the slider. +/// The title is displayed at the top left of the component. As users edit the lower or upper value in the text fields or move the thumbs, the slider continuously updates the bound values to reflect the thumbs’ positions. +/// +/// A single editable range slider is also supported. In this case, only the formatted upper value is displayed in a text field at the trailing end of the slider. +/// +/// The following example illustrates an editable range slider bound to the lower value `lowerValue` and the upper value `upperValue`. +/// The range slider uses the default range of `0` to `100`, with a default step of `1`. +/// By default, the lower value is displayed in a text field as the leading accessory view, while the upper value is shown in a text field as the trailing accessory view. +/// Both the lower thumb and upper thumb are clearly displayed on the slider track. +/// You can edit these values in the text fields to change the lower and upper values. +/// Alternatively, you can drag the lower thumb to adjust the lower value and drag the upper thumb to change the upper value. +/// The range slider does not display the value label at the top right of the slider by default. +/// +/// ```swift +/// @State private var lowerValue: Double = 20 +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Editable Range Slider", +/// lowerValue: $lowerValue, +/// upperValue: $upperValue, +/// description: "Simple editable range slider" +/// ) +/// ``` +/// +/// The following example illustrates a single editable range slider bound to the upper value `upperValue`. +/// The range slider uses the default range of `0` to `100`, with a default step of `1`. +/// By default, only the upper value is shown in a text field as the trailing accessory view and one thumb displayed on the slider track. +/// You can edit value in the text fields to change the upper values or drag the thumb to adjust the upper value. +/// The single range slider does not display the value label at the top right of the slider by default. +/// +/// ```swift +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Single Editable Range Slider", +/// upperValue: $upperValue, +/// description: "Simple Single Editable range slider" +/// ) +/// ``` +/// +/// Similar with standard slider, the range slider also allow you use the `range` parameter to specify the value range of the slider. +/// The `step` parameter allows you to define incremental steps along the slider's path. +/// The `decimalPlaces` parameter can be used to manage the decimal places of the slider's value. +/// By default, the range slider does not display the value label. +/// However, you can specify what you want to display in the `valueLabel` parameter to show the value label at the top right of the slider. +/// The `showsLeadingAccessory` and `showsTrailingAccessory` parameters control the display of the leading accessory view and trailing accessory view, respectively. +/// By default, the editable range slider uses a text field as the leading or trailing accessory view. +/// However, you can specify your own view in the `leadingAccessory` or `trailingAccessory` parameters to override the default text field. +/// The `showsLeadingAccessory` and `showsTrailingAccessory` parameters can be used to control the display of the respective accessory views. +/// The `onRangeValueChange` closure passed to the slider provides callbacks when the user drags the slider. +/// The `onValueChange` closure passed to the single editable slider provides callbacks when the user drags the slider. +/// +/// ```swift +/// @State private var lowerValue: Double = 20 +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Editable Range Slider", +/// lowerValue: $lowerValue, +/// upperValue: $upperValue, +/// range: 10...200, +/// step: 2.5, +/// decimalPlaces: 1, +/// description: "Simple editable range slider", +/// onRangeValueChange: { isEditing, lowerValue, upperValue in +/// if !isEditing { +/// print("Range Slider value was: " + String(format: "%.1f", lowerValue) + " - " + String(format: "%.1f", upperValue)) +/// } +/// } +/// ) +/// ``` +/// +/// The slider also uses the `step` to increase or decrease the value when a +/// VoiceOver user adjusts the slider with voice commands. +/// +// sourcery: CompositeComponent +protocol _FioriSliderComponent: _TitleComponent, _ValueLabelComponent, _RangeSliderControlComponent, _InformationViewComponent, _LeadingAccessoryComponent, _TrailingAccessoryComponent { + // sourcery: defaultValue = "true" + /// Indicates whether the slider is a range slider or not. The default value is `true`, meaning that the slider is a range slider. + var isRangeSlider: Bool { get } + + /// This optional format is used to format the displayed slider value in the value label view. It is also utilized for formatting the accessibility value, if provided. + var valueFormat: String? { get } + + /// The optional formats are used to format the lower and upper bound values of the range. They are utilized for formatting the accessibility values when you customize the range slider with your own leading and trailing accessory views, if provided. + var rangeFormat: (String, String)? { get } + + /// This optional format is used to format the displayed minimal value of standard slider's range or lower value of range slider in the leading accessory view. It is also utilized for formatting the accessibility value, if provided. + var leadingValueFormat: String? { get } + + /// This optional format is used to format the displayed maximal value of standard slider's range or upper value of range slider in the trailing accessory view. It is also utilized for formatting the accessibility value, if provided. + var trailingValueFormat: String? { get } + + // sourcery: defaultValue = "true" + /// Indicates whether the value label is to be displayed or not. The default value is `true` + var showsValueLabel: Bool { get } + + // sourcery: defaultValue = "true" + /// Indicates whether the leading accessory view is to be displayed or not. The default value is `true` + var showsLeadingAccessory: Bool { get } + + // sourcery: defaultValue = "true" + /// Indicates whether the trailing accessory view is to be displayed or not. The default value is `true` + var showsTrailingAccessory: Bool { get } + + /// An optional callback function is triggered when the user begins to drag the thumb along the standard slider's track to adjust its value. The first boolean property indicates whether the editing process has begun or ended, with `false` signifying that the editing has concluded. The second double property represents the newly adjusted slider value. + var onValueChange: ((Bool, Double) -> Void)? { get } + + /// An optional callback function is triggered when the focus state of a text field, which serves as a leading or trailing accessory for an editable slider, changes. The boolean parameter of the callback indicates the focus state of the text field. This can be useful for obtaining the focus state when customizing the editable slider. + var onEditFieldFocusStatusChange: ((Bool) -> Void)? { get } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/ActiveTrackStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/ActiveTrackStyle.fiori.swift new file mode 100644 index 000000000..95a893dc4 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/ActiveTrackStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct ActiveTrackBaseStyle: ActiveTrackStyle { + @ViewBuilder + public func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + // Add default layout here + configuration.activeTrack + } +} + +// Default fiori styles +public struct ActiveTrackFioriStyle: ActiveTrackStyle { + @ViewBuilder + public func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + ActiveTrack(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/FioriSliderStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/FioriSliderStyle.fiori.swift new file mode 100644 index 000000000..81c7663e3 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/FioriSliderStyle.fiori.swift @@ -0,0 +1,523 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment fhe following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct FioriSliderBaseStyle: FioriSliderStyle { + @Environment(\.isEnabled) private var isEnabled + @Environment(\.sizeCategory) private var sizeCategory + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + VStack { + if !configuration.title.isEmpty || configuration.showsValueLabel { + if configuration.isLargeSizeCategory(self.sizeCategory) { + VStack { + configuration.title + + if configuration.showsValueLabel { + Spacer().frame(height: configuration.title.isEmpty ? 0 : 4) + configuration.valueLabel + } + } + } else { + HStack { + configuration.title + + if configuration.showsValueLabel { + Spacer().frame(width: configuration.title.isEmpty ? 0 : 16) + configuration.valueLabel + } + } + } + + Spacer() + .frame(height: configuration.isRangeSlider ? 4 : 24) + } + + if self.sizeCategory == .accessibilityExtraExtraExtraLarge, configuration.leadingAccessory.isEmpty, configuration.trailingAccessory.isEmpty, configuration.isRangeSlider { // only for editable range slider + VStack { + if configuration.showsLeadingAccessory || configuration.showsTrailingAccessory { + HStack { + if configuration.showsLeadingAccessory { + configuration.leadingAccessory + } + if configuration.showsTrailingAccessory { + Spacer() + configuration.trailingAccessory + } + } + + Spacer().frame(height: 4) + } + + RangeSliderControl(configuration.getRangeSliderControlConfiguration()) + .disabled(!self.isEnabled) + } + .accessibilityElement(children: .contain) + } else { + HStack { + if configuration.showsLeadingAccessory { + configuration.leadingAccessory + Spacer().frame(width: 8) + } + + RangeSliderControl(configuration.getRangeSliderControlConfiguration()) + .disabled(!self.isEnabled) + + if configuration.showsTrailingAccessory { + Spacer().frame(width: 8) + configuration.trailingAccessory + } + } + .accessibilityElement(children: .contain) + } + + if !configuration._informationView.isEmpty { + Spacer() + .frame(height: 4) + configuration._informationView + } + } + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + .clearFioriSliderEditableAccessoryFocusOnTap() + } +} + +// Default fiori styles +extension FioriSliderFioriStyle { + struct ContentFioriStyle: FioriSliderStyle { + @StateObject private var stateObject = SliderStateObject() + @State private var sliderRowSize: CGSize = .zero + @Environment(\.isEnabled) var isEnabled + + func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + let slider = FioriSlider(configuration) + .overlay( + GeometryReader { geometry -> Color in + DispatchQueue.main.async { + self.sliderRowSize = geometry.size + } + return Color.clear + } + ) + .background(Color.preferredColor(.secondaryGroupedBackground)) + .environmentObject(self.stateObject) + .environmentObject(SliderModelObject(configuration: configuration, roundValueFormat: "%.\(configuration.decimalPlaces)f")) + .environment(\.roundValueFormat, "%.\(configuration.decimalPlaces)f") + .environment(\.sliderRowSize, self.sliderRowSize) + .accessibilityElement(children: configuration.isRangeSlider ? .contain : .combine) // To combine all accessibility label for all children for non-range slider + + if self.isEnabled, !configuration.isRangeSlider { + // Let the standard slider adjustable in enabled state by default. + return AnyView(slider.accessibilityAdjustableAction { direction in + configuration.getRangeSliderControlConfiguration().adjustThumbAccessibilityAction(direction: direction) + }) + } else { + return AnyView(slider) + } + } + } + + struct TitleFioriStyle: TitleStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.layoutDirection) var layoutDirection + @Environment(\.trailingTextFieldStyle) var trailingTextFieldStyle + @EnvironmentObject private var stateObject: SliderStateObject + + func makeBody(_ configuration: TitleConfiguration) -> some View { + Title(configuration) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.fiori(forTextStyle: .subheadline)) + .fontWeight(.semibold) + .foregroundStyle(self.isEnabled ? (self.stateObject.isFocused ? self.trailingTextFieldStyle.focusedBorderColor : Color.preferredColor(.primaryLabel)) : Color.preferredColor(.quaternaryLabel)) + .accessibilitySortPriority(self.fioriSliderConfiguration.isRangeSlider ? 7 : 0) // Need to set sort priority when the control was Range Slider + } + } + + struct ValueLabelFioriStyle: ValueLabelStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + @Environment(\.layoutDirection) var layoutDirection + @Environment(\.sizeCategory) private var sizeCategory + + func makeBody(_ configuration: ValueLabelConfiguration) -> some View { + let valueLabel = ValueLabel(configuration, fioriSliderConfiguration: fioriSliderConfiguration) + .font(.fiori(forTextStyle: .body)) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.primaryLabel) : Color.preferredColor(.quaternaryLabel)) + + guard !valueLabel.isEmpty else { + return AnyView(valueLabel) + } + + guard !self.fioriSliderConfiguration.title.isEmpty else { + // value label should always has alignment when there is no title display + return AnyView(valueLabel + .frame(maxWidth: .infinity, alignment: self.layoutDirection == .rightToLeft ? .leading : .trailing)) + } + + if self.fioriSliderConfiguration.isLargeSizeCategory(self.sizeCategory) { + // value label should has same alignment with title when the title display in large size mode since they were in one column not in one line + return AnyView(valueLabel + .frame(maxWidth: .infinity, alignment: .leading)) + } + + return AnyView(valueLabel) + } + } + + struct LowerThumbFioriStyle: LowerThumbStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + + func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + guard self.isEnabled, self.fioriSliderConfiguration.isRangeSlider, self.fioriSliderConfiguration.showsLowerThumb else { + return AnyView(LowerThumb(configuration)) + } + + return AnyView(LowerThumb(configuration) + .accessibilityLabel(NSLocalizedString("lower range slider thumb, current value is", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .accessibilityHint(NSLocalizedString("you can swipe up or down to change the lower value", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .accessibilityValue(String(format: self.fioriSliderConfiguration.leadingValueFormat ?? self.roundValueFormat, self.fioriSliderConfiguration.lowerValue)) + .accessibilitySortPriority(5) + .accessibilityAdjustableAction { direction in + switch direction { + case .decrement: + self.fioriSliderConfiguration.lowerValue = max(self.fioriSliderConfiguration.lowerValue - self.fioriSliderConfiguration.step, self.fioriSliderConfiguration.range.lowerBound) + case .increment: + self.fioriSliderConfiguration.lowerValue = min(self.fioriSliderConfiguration.lowerValue + self.fioriSliderConfiguration.step, self.fioriSliderConfiguration.range.upperBound) + @unknown default: + self.fioriSliderConfiguration.lowerValue = self.fioriSliderConfiguration.lowerValue + } + }) + } + } + + struct UpperThumbFioriStyle: UpperThumbStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + + func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + guard self.isEnabled, self.fioriSliderConfiguration.isRangeSlider, self.fioriSliderConfiguration.showsLowerThumb, self.fioriSliderConfiguration.showsUpperThumb else { + return AnyView(UpperThumb(configuration)) + } + + return AnyView(UpperThumb(configuration) + .accessibilityLabel(NSLocalizedString(self.fioriSliderConfiguration.showsLowerThumb ? "upper range slider thumb, current value is" : "slider thumb, current value is", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .accessibilityHint(NSLocalizedString(self.fioriSliderConfiguration.showsLowerThumb ? "you can swipe up or down to change the upper value" : "you can swipe up or down to change the value", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .accessibilityValue(String(format: self.fioriSliderConfiguration.trailingValueFormat ?? self.roundValueFormat, self.fioriSliderConfiguration.upperValue)) + .accessibilitySortPriority(4) + .accessibilityAdjustableAction { direction in + switch direction { + case .decrement: + self.fioriSliderConfiguration.upperValue = max(self.fioriSliderConfiguration.upperValue - self.fioriSliderConfiguration.step, self.fioriSliderConfiguration.range.lowerBound) + case .increment: + self.fioriSliderConfiguration.upperValue = min(self.fioriSliderConfiguration.upperValue + self.fioriSliderConfiguration.step, self.fioriSliderConfiguration.range.upperBound) + @unknown default: + self.fioriSliderConfiguration.upperValue = self.fioriSliderConfiguration.upperValue + } + }) + } + } + + struct ActiveTrackFioriStyle: ActiveTrackStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + + func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + ActiveTrack(configuration) + } + } + + struct InactiveTrackFioriStyle: InactiveTrackStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + + func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + InactiveTrack(configuration) + } + } + + struct IconFioriStyle: IconStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + + func makeBody(_ configuration: IconConfiguration) -> some View { + Icon(configuration) + .foregroundStyle(Color.preferredColor(.negativeLabel)) + .font(.fiori(forTextStyle: .footnote)) + } + } + + struct DescriptionFioriStyle: DescriptionStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + + func makeBody(_ configuration: DescriptionConfiguration) -> some View { + Description(configuration) + .font(.fiori(forTextStyle: .footnote)) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.tertiaryLabel).opacity(0.9) : Color.preferredColor(.quaternaryLabel)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + struct LeadingAccessoryFioriStyle: LeadingAccessoryStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + + func makeBody(_ configuration: LeadingAccessoryConfiguration) -> some View { + let isCustomEmpty = configuration.leadingAccessory.isEmpty + + let content = LeadingAccessory(configuration: configuration, fioriSliderConfiguration: fioriSliderConfiguration) + + if self.fioriSliderConfiguration.isRangeSlider, isCustomEmpty { // To use the text field for Range Slider + let labelHint = self.getEditableAccessibilityLabelHint() + return AnyView(content + .accessibilityHidden(!self.isEnabled) + .accessibilityLabel(labelHint.0) + .accessibilityHint(labelHint.1) + .accessibilityValue(self.getEditableAccessibilityValue()) + .accessibilitySortPriority(3)) + .typeErased + } else { + return AnyView(content + .font(Font.fiori(forTextStyle: .body)) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.tertiaryLabel).opacity(0.9) : Color.preferredColor(.separator).opacity(0.37))) + } + } + + func getEditableAccessibilityLabelHint() -> (String, String) { + guard self.isEnabled, self.fioriSliderConfiguration.showsLeadingAccessory else { + return ("", "") + } + + return (NSLocalizedString("lower value text field", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), NSLocalizedString("You can double tap to input a specific value", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + } + + func getEditableAccessibilityValue() -> String { + guard self.isEnabled, self.fioriSliderConfiguration.showsLeadingAccessory else { + return String(format: self.roundValueFormat, self.fioriSliderConfiguration.lowerValue) + } + + let format = NSLocalizedString("current value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + return String(format: format, String(format: self.roundValueFormat, self.fioriSliderConfiguration.lowerValue)) + } + } + + struct TrailingAccessoryFioriStyle: TrailingAccessoryStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + @Environment(\.sizeCategory) private var sizeCategory + + func makeBody(_ configuration: TrailingAccessoryConfiguration) -> some View { + let isCustomEmpty = configuration.trailingAccessory.isEmpty + + let content = TrailingAccessory(configuration: configuration, fioriSliderConfiguration: fioriSliderConfiguration) + + if self.fioriSliderConfiguration.isRangeSlider, isCustomEmpty { // To use the text field for Range Slider + let labelHint = self.getEditableAccessibilityLabelHint() + return AnyView(content + .accessibilityHidden(!self.isEnabled) + .accessibilityLabel(labelHint.0) + .accessibilityHint(labelHint.1) + .accessibilityValue(self.getEditableAccessibilityValue()) + .accessibilitySortPriority(2)) + .typeErased + } else { + return AnyView(content + .font(Font.fiori(forTextStyle: .body)) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.tertiaryLabel).opacity(0.9) : Color.preferredColor(.separator).opacity(0.37))) + } + } + + func getEditableAccessibilityLabelHint() -> (String, String) { + guard self.isEnabled, self.fioriSliderConfiguration.showsTrailingAccessory else { + return ("", "") + } + + return (NSLocalizedString(self.fioriSliderConfiguration.showsLeadingAccessory ? "upper value text field" : "slider value text field", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), NSLocalizedString("You can double tap to input a specific value", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + } + + func getEditableAccessibilityValue() -> String { + let format = NSLocalizedString("current value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + return String(format: format, String(format: self.roundValueFormat, self.fioriSliderConfiguration.upperValue)) + } + } + + struct RangeSliderControlFioriStyle: RangeSliderControlStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + @Environment(\.isEnabled) var isEnabled + @Environment(\.roundValueFormat) var roundValueFormat + + func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + guard self.fioriSliderConfiguration.isRangeSlider else { + return AnyView(RangeSliderControl(configuration)) + } + + let isEditableSlider = self.fioriSliderConfiguration.leadingAccessory.isEmpty && self.fioriSliderConfiguration.trailingAccessory.isEmpty + return AnyView(RangeSliderControl(configuration) + .accessibilityAdjustments(self.getAccessibility(self.fioriSliderConfiguration, isEditableSlider))) + } + + func getAccessibility(_ configuration: FioriSliderConfiguration, _ isEditableSlider: Bool) -> RangeSliderAccessibilityModel { + var accessibility = RangeSliderAccessibilityModel() + + if configuration.showsLowerThumb, configuration.showsUpperThumb { + let labelFormat = isEditableSlider ? (self.isEnabled ? NSLocalizedString("This is a range slider that is editable, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") : NSLocalizedString("This is a range slider that can't be edited, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) : + NSLocalizedString("This is a range slider, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + var lowerFormat = self.roundValueFormat + var upperFormat = self.roundValueFormat + if let format = configuration.rangeFormat { + lowerFormat = format.0 + upperFormat = format.1 + } + accessibility.label = String(format: labelFormat, String(format: lowerFormat, configuration.range.lowerBound), String(format: upperFormat, configuration.range.upperBound)) + + accessibility.value = String(format: NSLocalizedString("current lower value is %@, upper value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), String(format: configuration.leadingValueFormat ?? self.roundValueFormat, configuration.lowerValue), String(format: configuration.trailingValueFormat ?? self.roundValueFormat, configuration.upperValue)) + + } else if configuration.showsUpperThumb { + let labelFormat = isEditableSlider ? (self.isEnabled ? NSLocalizedString("This is a slider that is editable, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") : NSLocalizedString("This is a slider that can't be edited, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) : + NSLocalizedString("This is a slider, the minimum value is %@ and the maximum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + + var lowerFormat = self.roundValueFormat + var upperFormat = self.roundValueFormat + if let format = configuration.rangeFormat { + lowerFormat = format.0 + upperFormat = format.1 + } + + accessibility.label = String(format: labelFormat, String(format: lowerFormat, configuration.range.lowerBound), String(format: upperFormat, configuration.range.upperBound)) + + accessibility.value = String(format: NSLocalizedString("current value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), String(format: configuration.trailingValueFormat ?? self.roundValueFormat, configuration.upperValue)) + } + + // Only let the RangeSlider adjustable when only one thumb enabled. Otherwise, each thumb was adjustable. + accessibility.adjustable = self.isEnabled && configuration.showsLowerThumb != configuration.showsUpperThumb + return accessibility + } + } + + struct InformationViewFioriStyle: InformationViewStyle { + let fioriSliderConfiguration: FioriSliderConfiguration + + func makeBody(_ configuration: InformationViewConfiguration) -> some View { + InformationView(configuration) + } + } +} + +public extension FioriSlider { + /// Adjust the accessibility label and value for the standard slider. By default, the standard slider uses SwiftUI's default accessibility behavior to combine all subviews. + /// However, this function allows you to adjust the accessibility label and value for the standard slider. + /// + /// The values of the parameters `title`, `leadingLabel`, `trailingLabel`, and `description` are formatted as 'This is a %@ slider whose maximum value is %@ and minimum value is %@, %@' for the accessibility label. + /// The value of the parameter `value` is formatted as 'Current value is %@' for the accessibility value. + /// + /// - Parameters: + /// - title: The string used to display the title of the slider. + /// - value: An optional formatted value as the accessibility value. The default formatted slider value is used when it was nil. + /// - leadingLabel: An optional formatted string as the accessibility label of leading accessory view. The default formatted minimum value of range is used when it is nil. + /// - trailingLabel: An optional formatted string as the accessibility label of trailing accessory view. The default formatted maximum value of range is used when it is nil. + /// - description: An optional formatted string which append to the accessibility label + func accessibilityAdjustments(title: String, value: String? = nil, leadingLabel: String? = nil, trailingLabel: String? = nil, description: String? = nil) -> some View { + let value = value ?? String(format: self.valueFormat ?? "%.\(self.decimalPlaces)f", self.upperValue) + return self.modifier(StandardSliderAccessibilityModifier(value: self.$upperValue, label: title, formattedValue: value, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, leadingLabelFormat: leadingLabel ?? self.leadingValueFormat, trailingLabelFormat: trailingLabel ?? self.trailingValueFormat, description: description ?? "", canAdjust: !self.isRangeSlider)) + } +} + +extension FioriSliderConfiguration { + func isLargeSizeCategory(_ sizeCategory: ContentSizeCategory) -> Bool { + sizeCategory == .accessibilityMedium || sizeCategory == .accessibilityLarge || sizeCategory == .accessibilityExtraLarge + || sizeCategory == .accessibilityExtraExtraLarge || sizeCategory == .accessibilityExtraExtraExtraLarge + } + + func getRangeSliderControlConfiguration() -> RangeSliderControlConfiguration { + var onRangeValueChange: ((Bool, Double, Double) -> Void)? = self.onRangeValueChange + + if let onValueChange = self.onValueChange, !self.showsLowerThumb { + onRangeValueChange = { isEditing, _, upperValue in + onValueChange(isEditing, upperValue) + } + } + + return RangeSliderControlConfiguration(lowerThumb: self.lowerThumb, upperThumb: self.upperThumb, activeTrack: self.activeTrack, inactiveTrack: self.inactiveTrack, lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: onRangeValueChange) + } +} + +// To modify accessibility of the standard slider which wrapper the SwiftUI built-in slider since the limitation of style component(Don't change to get the string value from a view. For example, the title of the Slider) +struct StandardSliderAccessibilityModifier: ViewModifier { + @Binding var value: Double + var label: String + var formattedValue: String + var range: ClosedRange + var step: Double + var decimalPlaces: Int + var leadingLabelFormat: String? + var trailingLabelFormat: String? + var description: String + var canAdjust: Bool + + @Environment(\.isEnabled) private var isEnabled + + func body(content: Content) -> some View { + guard self.canAdjust else { + return AnyView(content) + } + let minValue = String(format: self.leadingLabelFormat ?? "%.\(self.decimalPlaces)f", self.range.lowerBound) + let maxValue = String(format: self.trailingLabelFormat ?? "%.\(self.decimalPlaces)f", self.range.upperBound) + + let labelFormat = NSLocalizedString("This is %@ slider whose maximum value is %@ and minimum value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + let valueFormat = NSLocalizedString("Current value is %@", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") + let label = String(format: labelFormat, label, maxValue, minValue) + (description.isEmpty ? "" : ". \(self.description)") + var updateContent = content + .accessibilityElement() + .accessibilityLabel(label) + .accessibilityValue(String(format: valueFormat, self.formattedValue)) + + if self.isEnabled { // To set adjustable action to make sure the accessibility trait was 'adjustable' only for enabled status + updateContent = updateContent.accessibilityAdjustableAction { direction in + switch direction { + case .decrement: + self.value = max(self.value - self.step, self.range.lowerBound) + case .increment: + self.value = min(self.value + self.step, self.range.upperBound) + @unknown default: + self.value = self.value + } + } + } + + return AnyView(updateContent) + } +} + +class SliderStateObject: ObservableObject { // To stored and observe some stateful variables and avoid multi-instance was initialized. + @Published var isFocused: Bool = false // present the focused state of the text field + + var editingLeadingValue: String? // To stored the current editing leading TextField's value + var editingTrailingValue: String? // To stored the current editing trailing TextField's value +} + +class SliderModelObject: ObservableObject { // The ObservableObject was used to stored stateless variables + @Published var leadingText: String + @Published var trailingText: String + + init(configuration: FioriSliderConfiguration, roundValueFormat: String) { + self.leadingText = String(format: roundValueFormat, configuration.lowerValue) + self.trailingText = String(format: roundValueFormat, configuration.upperValue) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/InactiveTrackStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/InactiveTrackStyle.fiori.swift new file mode 100644 index 000000000..19e9850bd --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/InactiveTrackStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct InactiveTrackBaseStyle: InactiveTrackStyle { + @ViewBuilder + public func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + // Add default layout here + configuration.inactiveTrack + } +} + +// Default fiori styles +public struct InactiveTrackFioriStyle: InactiveTrackStyle { + @ViewBuilder + public func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + InactiveTrack(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/LeadingAccessoryStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/LeadingAccessoryStyle.fiori.swift new file mode 100644 index 000000000..8e189267e --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/LeadingAccessoryStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct LeadingAccessoryBaseStyle: LeadingAccessoryStyle { + @ViewBuilder + public func makeBody(_ configuration: LeadingAccessoryConfiguration) -> some View { + // Add default layout here + configuration.leadingAccessory + } +} + +// Default fiori styles +public struct LeadingAccessoryFioriStyle: LeadingAccessoryStyle { + @ViewBuilder + public func makeBody(_ configuration: LeadingAccessoryConfiguration) -> some View { + LeadingAccessory(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/LowerThumbStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/LowerThumbStyle.fiori.swift new file mode 100644 index 000000000..d0cb3b363 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/LowerThumbStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct LowerThumbBaseStyle: LowerThumbStyle { + @ViewBuilder + public func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + // Add default layout here + configuration.lowerThumb + } +} + +// Default fiori styles +public struct LowerThumbFioriStyle: LowerThumbStyle { + @ViewBuilder + public func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + LowerThumb(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/RangeSliderControlStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/RangeSliderControlStyle.fiori.swift new file mode 100644 index 000000000..b62d1053e --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/RangeSliderControlStyle.fiori.swift @@ -0,0 +1,288 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct RangeSliderControlBaseStyle: RangeSliderControlStyle { + @Environment(\.layoutDirection) private var layoutDirection + @Environment(\.isEnabled) private var isEnabled + @Environment(\.roundValueFormat) private var roundValueFormat + @Environment(\.rangeSliderAccessibility) private var rangeSliderAccessibility + + @State private var criticalValue: Double? // To stored the current lower/upper value during upper/lower thumb moving and make sure the lower/upper value can be exchanged. + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + GeometryReader { geometry in + ZStack { + // Invisible element to allow accessibility focusing the whole track + let accessibilityColorHStack = Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityElement() + .accessibilityLabel(self.rangeSliderAccessibility.label ?? "") + .accessibilityHint(self.rangeSliderAccessibility.hint ?? "") + .accessibilityValue(self.rangeSliderAccessibility.value ?? "") + .accessibilitySortPriority(self.rangeSliderAccessibility.sortPriority) + + if self.rangeSliderAccessibility.adjustable { + // Set it is .adjustable when there is only one thumb in enabled state + accessibilityColorHStack + .accessibilityAdjustableAction { direction in + configuration.adjustThumbAccessibilityAction(direction: direction) + } + } else { + accessibilityColorHStack + } + + let effectiveLowerValue = min(configuration.lowerValue, configuration.upperValue) + let effectiveUpperValue = max(configuration.lowerValue, configuration.upperValue) + + let upperValueWidth = self.xOffsetFor(min(effectiveUpperValue, configuration.range.upperBound), in: geometry.size.width, configuration: configuration) + let lowerValueWidth = self.xOffsetFor(max(effectiveLowerValue, configuration.range.lowerBound), in: geometry.size.width, configuration: configuration) + + let width = upperValueWidth >= lowerValueWidth ? (upperValueWidth - lowerValueWidth) : (lowerValueWidth - upperValueWidth) + + // Inactive track + configuration.inactiveTrack + + // Active track + configuration.activeTrack + .frame(width: width) + .position( + x: (self.xOffsetFor(max(effectiveLowerValue, configuration.range.lowerBound), in: geometry.size.width, configuration: configuration) + self.xOffsetFor(min(effectiveUpperValue, configuration.range.upperBound), in: geometry.size.width, configuration: configuration)) / 2, + y: geometry.size.height / 2 + ) + + if configuration.showsLowerThumb { + self.lowerThumb(effectiveLowerValue, allowedSize: geometry.size, configuration: configuration) + } + + if configuration.showsUpperThumb { + self.upperThumb(effectiveUpperValue, allowedSize: geometry.size, configuration: configuration) + } + } + } + .frame(height: 44) + } + + func xOffsetFor(_ value: Double, in width: CGFloat, offset: Bool = false, configuration: RangeSliderControlConfiguration) -> CGFloat { + let rangeWidth = configuration.range.upperBound - configuration.range.lowerBound + let valueOffset = value - configuration.range.lowerBound + let offsetPadding: CGFloat = offset ? configuration.thumbHalfWidth : 0 + return CGFloat(valueOffset / rangeWidth) * (width - 2 * configuration.thumbHalfWidth) + offsetPadding + } + + func valueFrom(x: CGFloat, in width: CGFloat, configuration: RangeSliderControlConfiguration) -> Double { + // Invert the x position for RTL layout + let adjustedX = self.layoutDirection == .rightToLeft ? width - x : x + let proportion = max(0, min(1, (adjustedX - configuration.thumbHalfWidth) / (width - 2 * configuration.thumbHalfWidth))) + let rawValue = Double(proportion) * (configuration.range.upperBound - configuration.range.lowerBound) + configuration.range.lowerBound + if rawValue == configuration.range.upperBound || rawValue == configuration.range.lowerBound { + return rawValue + } else { + return round(rawValue / configuration.step) * configuration.step // Stepped value + } + } + + func lowerThumb(_ effectiveLowerValue: Double, allowedSize: CGSize, configuration: RangeSliderControlConfiguration) -> some View { + configuration.lowerThumb + .position( + x: self.xOffsetFor(max(effectiveLowerValue, configuration.range.lowerBound), in: allowedSize.width, offset: true, configuration: configuration), + y: allowedSize.height / 2 + ) + .gesture( + self.isEnabled ? + DragGesture() + .onEnded { _ in + self.criticalValue = nil + if let onRangeValueChange = configuration.onRangeValueChange { + onRangeValueChange(false, configuration.lowerValue, configuration.upperValue) + } + } + .onChanged { value in + if self.criticalValue == nil { + self.criticalValue = configuration.upperValue + } + + if let criticalValue = self.criticalValue { + let newValue = self.valueFrom(x: value.location.x, in: allowedSize.width, configuration: configuration) + if newValue <= criticalValue { + configuration.lowerValue = newValue + if configuration.upperValue != criticalValue { + configuration.upperValue = criticalValue + } + } else { + configuration.upperValue = newValue + if configuration.lowerValue != criticalValue { + configuration.lowerValue = criticalValue + } + } + } + } + : nil + ) + } + + func upperThumb(_ effectiveUpperValue: Double, allowedSize: CGSize, configuration: RangeSliderControlConfiguration) -> some View { + configuration.upperThumb + .position( + x: self.xOffsetFor(min(effectiveUpperValue, configuration.range.upperBound), in: allowedSize.width, offset: true, configuration: configuration), + y: allowedSize.height / 2 + ) + .gesture( + self.isEnabled ? + DragGesture() + .onEnded { _ in + self.criticalValue = nil + if let onRangeValueChange = configuration.onRangeValueChange { + onRangeValueChange(false, configuration.lowerValue, configuration.upperValue) + } + } + .onChanged { value in + if self.criticalValue == nil { + self.criticalValue = configuration.lowerValue + } + + if let criticalValue = self.criticalValue { + let newValue = self.valueFrom(x: value.location.x, in: allowedSize.width, configuration: configuration) + if newValue >= criticalValue { + configuration.upperValue = newValue + if configuration.lowerValue != criticalValue { + configuration.lowerValue = criticalValue + } + } else { + configuration.lowerValue = newValue + if configuration.upperValue != criticalValue { + configuration.upperValue = criticalValue + } + } + } + } + : nil + ) + } +} + +// Default fiori styles +extension RangeSliderControlFioriStyle { + struct ContentFioriStyle: RangeSliderControlStyle { + func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + } + } + + struct LowerThumbFioriStyle: LowerThumbStyle { + let rangeSliderControlConfiguration: RangeSliderControlConfiguration + @Environment(\.isEnabled) private var isEnabled + + func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + LowerThumb(configuration) + .foregroundStyle(Color.white) + .frame(width: self.rangeSliderControlConfiguration.thumbHalfWidth * 2, height: self.rangeSliderControlConfiguration.thumbHalfWidth * 2) + .shadow(color: .black.opacity(0.2), radius: self.rangeSliderControlConfiguration.thumbHalfWidth * 0.4, x: 0, y: 3) + } + } + + struct UpperThumbFioriStyle: UpperThumbStyle { + let rangeSliderControlConfiguration: RangeSliderControlConfiguration + + func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + UpperThumb(configuration) + .frame(width: self.rangeSliderControlConfiguration.thumbHalfWidth * 2, height: self.rangeSliderControlConfiguration.thumbHalfWidth * 2) + .foregroundStyle(Color.white) + .shadow(color: .black.opacity(0.2), radius: self.rangeSliderControlConfiguration.thumbHalfWidth * 0.4, x: 0, y: 3) + } + } + + struct ActiveTrackFioriStyle: ActiveTrackStyle { + let rangeSliderControlConfiguration: RangeSliderControlConfiguration + @Environment(\.isEnabled) private var isEnabled + + func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + ActiveTrack(configuration) + .frame(height: 4) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.tintColor) : Color.preferredColor(.tintColor).opacity(0.5)) + } + } + + struct InactiveTrackFioriStyle: InactiveTrackStyle { + let rangeSliderControlConfiguration: RangeSliderControlConfiguration + @Environment(\.isEnabled) private var isEnabled + + func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + InactiveTrack(configuration) + .frame(height: 4) + .foregroundStyle(self.isEnabled ? Color.preferredColor(.secondaryFill) : Color.preferredColor(.secondaryFill).opacity(0.5)) + } + } +} + +extension RangeSliderControl { + func accessibilityAdjustments(_ model: RangeSliderAccessibilityModel) -> some View { + self.modifier(RangeSliderAccessibilityModifier(model: model)) + } +} + +extension RangeSliderControlConfiguration { + func adjustThumbAccessibilityAction(direction: AccessibilityAdjustmentDirection) { + switch direction { + case .decrement: + if self.showsLowerThumb { + self.lowerValue = max(self.lowerValue - self.step, self.range.lowerBound) + } else { + self.upperValue = max(self.upperValue - self.step, self.range.lowerBound) + } + case .increment: + if self.showsLowerThumb { + self.lowerValue = min(self.lowerValue + self.step, self.range.upperBound) + } else { + self.upperValue = min(self.upperValue + self.step, self.range.upperBound) + } + @unknown default: + if self.showsLowerThumb { + self.lowerValue = self.lowerValue + } else { + self.upperValue = self.upperValue + } + } + } +} + +struct RangeSliderAccessibilityModel { + var label: String? + var value: String? + var hint: String? + var adjustable: Bool = true + var sortPriority: Double = 6 +} + +// To change the Range Slider accessibility +struct RangeSliderAccessibilityModifier: ViewModifier { + var model: RangeSliderAccessibilityModel + + func body(content: Content) -> some View { + content.environment(\.rangeSliderAccessibility, self.model) + } +} + +struct RangeSliderAccessibilityModelKey: EnvironmentKey { + static let defaultValue: RangeSliderAccessibilityModel = .init() +} + +extension EnvironmentValues { + var rangeSliderAccessibility: RangeSliderAccessibilityModel { + get { self[RangeSliderAccessibilityModelKey.self] } + set { self[RangeSliderAccessibilityModelKey.self] = newValue } + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/TrailingAccessoryStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/TrailingAccessoryStyle.fiori.swift new file mode 100644 index 000000000..88bfdd709 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/TrailingAccessoryStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct TrailingAccessoryBaseStyle: TrailingAccessoryStyle { + @ViewBuilder + public func makeBody(_ configuration: TrailingAccessoryConfiguration) -> some View { + // Add default layout here + configuration.trailingAccessory + } +} + +// Default fiori styles +public struct TrailingAccessoryFioriStyle: TrailingAccessoryStyle { + @ViewBuilder + public func makeBody(_ configuration: TrailingAccessoryConfiguration) -> some View { + TrailingAccessory(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/UpperThumbStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/UpperThumbStyle.fiori.swift new file mode 100644 index 000000000..efde024b2 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/UpperThumbStyle.fiori.swift @@ -0,0 +1,35 @@ +import FioriThemeManager + +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/** + This file provides default fiori style for the component. + + 1. Uncomment the following code. + 2. Implement layout and style in corresponding places. + 3. Delete `.generated` from file name. + 4. Move this file to `_FioriStyles` folder under `FioriSwiftUICore`. + */ + +// Base Layout style +public struct UpperThumbBaseStyle: UpperThumbStyle { + @ViewBuilder + public func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + // Add default layout here + configuration.upperThumb + } +} + +// Default fiori styles +public struct UpperThumbFioriStyle: UpperThumbStyle { + @ViewBuilder + public func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + UpperThumb(configuration) + // Add default style here + // .foregroundStyle(Color.preferredColor(<#fiori color#>)) + // .font(.fiori(forTextStyle: <#fiori font#>)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrack.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrack.generated.swift new file mode 100644 index 000000000..efb0df8db --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrack.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct ActiveTrack { + let activeTrack: any View + + @Environment(\.activeTrackStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder activeTrack: () -> any View) { + self.activeTrack = activeTrack() + } +} + +public extension ActiveTrack { + init(activeTrack: any Shape = Capsule()) { + self.init(activeTrack: { activeTrack }) + } +} + +public extension ActiveTrack { + init(_ configuration: ActiveTrackConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: ActiveTrackConfiguration, shouldApplyDefaultStyle: Bool) { + self.activeTrack = configuration.activeTrack + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension ActiveTrack: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(activeTrack: .init(self.activeTrack))).typeErased + .transformEnvironment(\.activeTrackStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension ActiveTrack { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + ActiveTrack(.init(activeTrack: .init(self.activeTrack))) + .shouldApplyDefaultStyle(false) + .activeTrackStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrackStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrackStyle.generated.swift new file mode 100644 index 000000000..ef3076ff5 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ActiveTrack/ActiveTrackStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol ActiveTrackStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: ActiveTrackConfiguration) -> Body +} + +struct AnyActiveTrackStyle: ActiveTrackStyle { + let content: (ActiveTrackConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (ActiveTrackConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct ActiveTrackConfiguration { + public let activeTrack: ActiveTrack + + public typealias ActiveTrack = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSlider.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSlider.generated.swift new file mode 100644 index 000000000..281b18143 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSlider.generated.swift @@ -0,0 +1,371 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +/// The `FioriSlider` is a SwiftUI component that provides both a standard slider and a range slider. +/// The standard slider allows users to select a single value, while the range slider allows users to select a range of values with two thumbs. +/// +/// ## Usage +/// +/// ### Standard Slider: +/// +/// A standard slider consists of a title, a bound value, and a "thumb" (an image that users can drag along a linear "track". +/// The track represents a continuum between two extremes: a minimum and a maximum value. +/// By default, the formatted minimum value is displayed at the leading end of the slider, and the formatted maximum value is displayed at the trailing end of the slider. +/// +/// The title is displayed at the top left of the component, while the bound value is displayed at the top right. As users move the thumb, the slider continuously updates its bound value to reflect the thumb’s position. +/// +/// The following example illustrates a standard slider bound to the value `speed`. The slider uses the default range of `0` to `100`, with a default step of `1`. +/// The minimum value of the range is displayed as the leading accessory view label, while the maximum value is shown as the trailing accessory view label. +/// As the slider updates the `speed` value, the updated value is displayed in a label at the top right of the slider. +/// +/// ```swift +/// @State private var speed: Double = 20 +/// +/// FioriSlider( +/// title: "Speed Limit", +/// value: $speed, +/// description: "Simple standard slider" +/// ) +/// ``` +/// +/// You can also use the `range` parameter to specify the value range of the slider. +/// The `step` parameter allows you to define incremental steps along the slider's path. +/// The `decimalPlaces` parameter can be used to manage the decimal places of the slider's value. +/// To format the bound value for display, use the `valueFormat` parameter. +/// The `leadingValueFormat` parameter customizes the leading value label, which displays the minimum value of the range. +/// Similarly, the `trailingValueFormat` parameter customizes the trailing value label, which displays the maximum value of the range. +/// Additionally, you can use the `showsValueLabel`, `showsLeadingAccessory`, and `showsTrailingAccessory` parameters to control the display of the related labels. +/// The `onValueChange` closure passed to the slider provides callbacks when the user drags the slider. +/// +/// ```swift +/// @State private var speed: Double = 20 +/// +/// FioriSlider( +/// title: "Speed Limit", +/// value: $speed, +/// range: 10...200, +/// step: 2.5, +/// decimalPlaces: 1, +/// description: "Simple standard slider", +/// valueLabelFormat: "%.1f KM", +/// leadingLabelFormat: "%.1f KM", +/// trailingLabelFormat: "%.1f KM", +/// onValueChange: { isEditing, newSpeed in +/// if !isEditing { +/// print("The speed was changed to: " + String(format: "%.1f", value)) +/// } +/// } +/// ) +/// ``` +/// The example above illustrates a standard slider with a range of `10` to `200` and a step increment of `2.5`. +/// Therefore, the slider's increments would be `10`, `12.5`, `15`, and so on. +/// At the same time, the minimum value of the range is formatted and displayed as `10.0 KM`. +/// Similarly, the maximum value of the range is formatted and displayed as `200.0 KM`. +/// The updated value can be received within the `onValueChange` closure callback when the user drags the slider. +/// +/// The slider also uses the `step` to increase or decrease the value when a +/// VoiceOver user adjusts the slider with voice commands. +/// +/// The `FioriSlider` supports a modifier called `accessibilityAdjustments`, which allows you to adjust the accessibility settings for a standard slider according to the Fiori Slider guidelines. +/// +/// ### Range Slider: +/// +/// A range slider consists of a title, a bound lower value, a bound upper value, and two "thumbs" (images that users can drag along a linear "track"). +/// The track represents a continuum between two extremes: a minimum and a maximum value. +/// By default, the formatted lower value is displayed in a text field at the leading end of the slider, and the formatted upper value is displayed in a text field at the trailing end of the slider. +/// The title is displayed at the top left of the component. As users edit the lower or upper value in the text fields or move the thumbs, the slider continuously updates the bound values to reflect the thumbs’ positions. +/// +/// A single editable range slider is also supported. In this case, only the formatted upper value is displayed in a text field at the trailing end of the slider. +/// +/// The following example illustrates an editable range slider bound to the lower value `lowerValue` and the upper value `upperValue`. +/// The range slider uses the default range of `0` to `100`, with a default step of `1`. +/// By default, the lower value is displayed in a text field as the leading accessory view, while the upper value is shown in a text field as the trailing accessory view. +/// Both the lower thumb and upper thumb are clearly displayed on the slider track. +/// You can edit these values in the text fields to change the lower and upper values. +/// Alternatively, you can drag the lower thumb to adjust the lower value and drag the upper thumb to change the upper value. +/// The range slider does not display the value label at the top right of the slider by default. +/// +/// ```swift +/// @State private var lowerValue: Double = 20 +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Editable Range Slider", +/// lowerValue: $lowerValue, +/// upperValue: $upperValue, +/// description: "Simple editable range slider" +/// ) +/// ``` +/// +/// The following example illustrates a single editable range slider bound to the upper value `upperValue`. +/// The range slider uses the default range of `0` to `100`, with a default step of `1`. +/// By default, only the upper value is shown in a text field as the trailing accessory view and one thumb displayed on the slider track. +/// You can edit value in the text fields to change the upper values or drag the thumb to adjust the upper value. +/// The single range slider does not display the value label at the top right of the slider by default. +/// +/// ```swift +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Single Editable Range Slider", +/// upperValue: $upperValue, +/// description: "Simple Single Editable range slider" +/// ) +/// ``` +/// +/// Similar with standard slider, the range slider also allow you use the `range` parameter to specify the value range of the slider. +/// The `step` parameter allows you to define incremental steps along the slider's path. +/// The `decimalPlaces` parameter can be used to manage the decimal places of the slider's value. +/// By default, the range slider does not display the value label. +/// However, you can specify what you want to display in the `valueLabel` parameter to show the value label at the top right of the slider. +/// The `showsLeadingAccessory` and `showsTrailingAccessory` parameters control the display of the leading accessory view and trailing accessory view, respectively. +/// By default, the editable range slider uses a text field as the leading or trailing accessory view. +/// However, you can specify your own view in the `leadingAccessory` or `trailingAccessory` parameters to override the default text field. +/// The `showsLeadingAccessory` and `showsTrailingAccessory` parameters can be used to control the display of the respective accessory views. +/// The `onRangeValueChange` closure passed to the slider provides callbacks when the user drags the slider. +/// The `onValueChange` closure passed to the single editable slider provides callbacks when the user drags the slider. +/// +/// ```swift +/// @State private var lowerValue: Double = 20 +/// @State private var upperValue: Double = 40 +/// +/// FioriSlider( +/// title: "Editable Range Slider", +/// lowerValue: $lowerValue, +/// upperValue: $upperValue, +/// range: 10...200, +/// step: 2.5, +/// decimalPlaces: 1, +/// description: "Simple editable range slider", +/// onRangeValueChange: { isEditing, lowerValue, upperValue in +/// if !isEditing { +/// print("Range Slider value was: " + String(format: "%.1f", lowerValue) + " - " + String(format: "%.1f", upperValue)) +/// } +/// } +/// ) +/// ``` +/// +/// The slider also uses the `step` to increase or decrease the value when a +/// VoiceOver user adjusts the slider with voice commands. +/// +public struct FioriSlider { + let title: any View + let valueLabel: any View + let lowerThumb: any View + let upperThumb: any View + let activeTrack: any View + let inactiveTrack: any View + /// The lower value of range slider. + @Binding var lowerValue: Double + /// The upper value of range slider + @Binding var upperValue: Double + /// The range of the slider values. The default is `0...100`. + let range: ClosedRange + /// The distance between each valid value. The default is `1` + let step: Double + /// This property specifies the number of digits that should appear after the decimal point in the Double value for slider value or lower/upper value for range slider . It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0` + let decimalPlaces: Int + /// The half-width of the thumb. This value only takes effect for a range slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + let thumbHalfWidth: CGFloat + /// Indicates whether the lower thumb is to be displayed or not. The default value is `true` + let showsLowerThumb: Bool + /// Indicates whether the upper thumb is to be displayed or not. The default value is `true` + let showsUpperThumb: Bool + /// An optional callback function that is triggered when the user begins to drag either the lower or upper thumb along the range slider's track or edit the value in text field to adjust the slider's values. The first boolean parameter indicates whether the editing has begun or ended, with `false` signifying that the editing has ended. The second parameter is a double that represents the updated lower value, while the third parameter (also a double) represents the updated upper value. + let onRangeValueChange: ((Bool, Double, Double) -> Void)? + let icon: any View + let description: any View + let leadingAccessory: any View + let trailingAccessory: any View + /// Indicates whether the slider is a range slider or not. The default value is `true`, meaning that the slider is a range slider. + let isRangeSlider: Bool + /// This optional format is used to format the displayed slider value in the value label view. It is also utilized for formatting the accessibility value, if provided. + let valueFormat: String? + /// The optional formats are used to format the lower and upper bound values of the range. They are utilized for formatting the accessibility values when you customize the range slider with your own leading and trailing accessory views, if provided. + let rangeFormat: (String, String)? + /// This optional format is used to format the displayed minimal value of standard slider's range or lower value of range slider in the leading accessory view. It is also utilized for formatting the accessibility value, if provided. + let leadingValueFormat: String? + /// This optional format is used to format the displayed maximal value of standard slider's range or upper value of range slider in the trailing accessory view. It is also utilized for formatting the accessibility value, if provided. + let trailingValueFormat: String? + /// Indicates whether the value label is to be displayed or not. The default value is `true` + let showsValueLabel: Bool + /// Indicates whether the leading accessory view is to be displayed or not. The default value is `true` + let showsLeadingAccessory: Bool + /// Indicates whether the trailing accessory view is to be displayed or not. The default value is `true` + let showsTrailingAccessory: Bool + /// An optional callback function is triggered when the user begins to drag the thumb along the standard slider's track to adjust its value. The first boolean property indicates whether the editing process has begun or ended, with `false` signifying that the editing has concluded. The second double property represents the newly adjusted slider value. + let onValueChange: ((Bool, Double) -> Void)? + /// An optional callback function is triggered when the focus state of a text field, which serves as a leading or trailing accessory for an editable slider, changes. The boolean parameter of the callback indicates the focus state of the text field. This can be useful for obtaining the focus state when customizing the editable slider. + let onEditFieldFocusStatusChange: ((Bool) -> Void)? + + @Environment(\.fioriSliderStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder title: () -> any View, + @ViewBuilder valueLabel: () -> any View = { EmptyView() }, + @ViewBuilder lowerThumb: () -> any View, + @ViewBuilder upperThumb: () -> any View, + @ViewBuilder activeTrack: () -> any View, + @ViewBuilder inactiveTrack: () -> any View, + lowerValue: Binding, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + thumbHalfWidth: CGFloat = 14, + showsLowerThumb: Bool = true, + showsUpperThumb: Bool = true, + onRangeValueChange: ((Bool, Double, Double) -> Void)? = nil, + @ViewBuilder icon: () -> any View = { EmptyView() }, + @ViewBuilder description: () -> any View = { EmptyView() }, + @ViewBuilder leadingAccessory: () -> any View = { EmptyView() }, + @ViewBuilder trailingAccessory: () -> any View = { EmptyView() }, + isRangeSlider: Bool = true, + valueFormat: String? = nil, + rangeFormat: (String, String)? = nil, + leadingValueFormat: String? = nil, + trailingValueFormat: String? = nil, + showsValueLabel: Bool = true, + showsLeadingAccessory: Bool = true, + showsTrailingAccessory: Bool = true, + onValueChange: ((Bool, Double) -> Void)? = nil, + onEditFieldFocusStatusChange: ((Bool) -> Void)? = nil) + { + self.title = Title(title: title) + self.valueLabel = ValueLabel(valueLabel: valueLabel) + self.lowerThumb = LowerThumb(lowerThumb: lowerThumb) + self.upperThumb = UpperThumb(upperThumb: upperThumb) + self.activeTrack = ActiveTrack(activeTrack: activeTrack) + self.inactiveTrack = InactiveTrack(inactiveTrack: inactiveTrack) + self._lowerValue = lowerValue + self._upperValue = upperValue + self.range = range + self.step = step + self.decimalPlaces = decimalPlaces + self.thumbHalfWidth = thumbHalfWidth + self.showsLowerThumb = showsLowerThumb + self.showsUpperThumb = showsUpperThumb + self.onRangeValueChange = onRangeValueChange + self.icon = Icon(icon: icon) + self.description = Description(description: description) + self.leadingAccessory = LeadingAccessory(leadingAccessory: leadingAccessory) + self.trailingAccessory = TrailingAccessory(trailingAccessory: trailingAccessory) + self.isRangeSlider = isRangeSlider + self.valueFormat = valueFormat + self.rangeFormat = rangeFormat + self.leadingValueFormat = leadingValueFormat + self.trailingValueFormat = trailingValueFormat + self.showsValueLabel = showsValueLabel + self.showsLeadingAccessory = showsLeadingAccessory + self.showsTrailingAccessory = showsTrailingAccessory + self.onValueChange = onValueChange + self.onEditFieldFocusStatusChange = onEditFieldFocusStatusChange + } +} + +public extension FioriSlider { + init(title: AttributedString, + valueLabel: AttributedString? = nil, + lowerThumb: any Shape = Circle(), + upperThumb: any Shape = Circle(), + activeTrack: any Shape = Capsule(), + inactiveTrack: any Shape = Capsule(), + lowerValue: Binding, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + thumbHalfWidth: CGFloat = 14, + showsLowerThumb: Bool = true, + showsUpperThumb: Bool = true, + onRangeValueChange: ((Bool, Double, Double) -> Void)? = nil, + icon: Image? = nil, + description: AttributedString? = nil, + @ViewBuilder leadingAccessory: () -> any View = { EmptyView() }, + @ViewBuilder trailingAccessory: () -> any View = { EmptyView() }, + isRangeSlider: Bool = true, + valueFormat: String? = nil, + rangeFormat: (String, String)? = nil, + leadingValueFormat: String? = nil, + trailingValueFormat: String? = nil, + showsValueLabel: Bool = true, + showsLeadingAccessory: Bool = true, + showsTrailingAccessory: Bool = true, + onValueChange: ((Bool, Double) -> Void)? = nil, + onEditFieldFocusStatusChange: ((Bool) -> Void)? = nil) + { + self.init(title: { Text(title) }, valueLabel: { OptionalText(valueLabel) }, lowerThumb: { lowerThumb }, upperThumb: { upperThumb }, activeTrack: { activeTrack }, inactiveTrack: { inactiveTrack }, lowerValue: lowerValue, upperValue: upperValue, range: range, step: step, decimalPlaces: decimalPlaces, thumbHalfWidth: thumbHalfWidth, showsLowerThumb: showsLowerThumb, showsUpperThumb: showsUpperThumb, onRangeValueChange: onRangeValueChange, icon: { icon }, description: { OptionalText(description) }, leadingAccessory: leadingAccessory, trailingAccessory: trailingAccessory, isRangeSlider: isRangeSlider, valueFormat: valueFormat, rangeFormat: rangeFormat, leadingValueFormat: leadingValueFormat, trailingValueFormat: trailingValueFormat, showsValueLabel: showsValueLabel, showsLeadingAccessory: showsLeadingAccessory, showsTrailingAccessory: showsTrailingAccessory, onValueChange: onValueChange, onEditFieldFocusStatusChange: onEditFieldFocusStatusChange) + } +} + +public extension FioriSlider { + init(_ configuration: FioriSliderConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: FioriSliderConfiguration, shouldApplyDefaultStyle: Bool) { + self.title = configuration.title + self.valueLabel = configuration.valueLabel + self.lowerThumb = configuration.lowerThumb + self.upperThumb = configuration.upperThumb + self.activeTrack = configuration.activeTrack + self.inactiveTrack = configuration.inactiveTrack + self._lowerValue = configuration.$lowerValue + self._upperValue = configuration.$upperValue + self.range = configuration.range + self.step = configuration.step + self.decimalPlaces = configuration.decimalPlaces + self.thumbHalfWidth = configuration.thumbHalfWidth + self.showsLowerThumb = configuration.showsLowerThumb + self.showsUpperThumb = configuration.showsUpperThumb + self.onRangeValueChange = configuration.onRangeValueChange + self.icon = configuration.icon + self.description = configuration.description + self.leadingAccessory = configuration.leadingAccessory + self.trailingAccessory = configuration.trailingAccessory + self.isRangeSlider = configuration.isRangeSlider + self.valueFormat = configuration.valueFormat + self.rangeFormat = configuration.rangeFormat + self.leadingValueFormat = configuration.leadingValueFormat + self.trailingValueFormat = configuration.trailingValueFormat + self.showsValueLabel = configuration.showsValueLabel + self.showsLeadingAccessory = configuration.showsLeadingAccessory + self.showsTrailingAccessory = configuration.showsTrailingAccessory + self.onValueChange = configuration.onValueChange + self.onEditFieldFocusStatusChange = configuration.onEditFieldFocusStatusChange + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension FioriSlider: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(title: .init(self.title), valueLabel: .init(self.valueLabel), lowerThumb: .init(self.lowerThumb), upperThumb: .init(self.upperThumb), activeTrack: .init(self.activeTrack), inactiveTrack: .init(self.inactiveTrack), lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: self.onRangeValueChange, icon: .init(self.icon), description: .init(self.description), leadingAccessory: .init(self.leadingAccessory), trailingAccessory: .init(self.trailingAccessory), isRangeSlider: self.isRangeSlider, valueFormat: self.valueFormat, rangeFormat: self.rangeFormat, leadingValueFormat: self.leadingValueFormat, trailingValueFormat: self.trailingValueFormat, showsValueLabel: self.showsValueLabel, showsLeadingAccessory: self.showsLeadingAccessory, showsTrailingAccessory: self.showsTrailingAccessory, onValueChange: self.onValueChange, onEditFieldFocusStatusChange: self.onEditFieldFocusStatusChange)).typeErased + .transformEnvironment(\.fioriSliderStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension FioriSlider { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + FioriSlider(.init(title: .init(self.title), valueLabel: .init(self.valueLabel), lowerThumb: .init(self.lowerThumb), upperThumb: .init(self.upperThumb), activeTrack: .init(self.activeTrack), inactiveTrack: .init(self.inactiveTrack), lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: self.onRangeValueChange, icon: .init(self.icon), description: .init(self.description), leadingAccessory: .init(self.leadingAccessory), trailingAccessory: .init(self.trailingAccessory), isRangeSlider: self.isRangeSlider, valueFormat: self.valueFormat, rangeFormat: self.rangeFormat, leadingValueFormat: self.leadingValueFormat, trailingValueFormat: self.trailingValueFormat, showsValueLabel: self.showsValueLabel, showsLeadingAccessory: self.showsLeadingAccessory, showsTrailingAccessory: self.showsTrailingAccessory, onValueChange: self.onValueChange, onEditFieldFocusStatusChange: self.onEditFieldFocusStatusChange)) + .shouldApplyDefaultStyle(false) + .fioriSliderStyle(FioriSliderFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSliderStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSliderStyle.generated.swift new file mode 100644 index 000000000..93569cebd --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FioriSlider/FioriSliderStyle.generated.swift @@ -0,0 +1,83 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol FioriSliderStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: FioriSliderConfiguration) -> Body +} + +struct AnyFioriSliderStyle: FioriSliderStyle { + let content: (FioriSliderConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (FioriSliderConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct FioriSliderConfiguration { + public let title: Title + public let valueLabel: ValueLabel + public let lowerThumb: LowerThumb + public let upperThumb: UpperThumb + public let activeTrack: ActiveTrack + public let inactiveTrack: InactiveTrack + @Binding public var lowerValue: Double + @Binding public var upperValue: Double + public let range: ClosedRange + public let step: Double + public let decimalPlaces: Int + public let thumbHalfWidth: CGFloat + public let showsLowerThumb: Bool + public let showsUpperThumb: Bool + public let onRangeValueChange: ((Bool, Double, Double) -> Void)? + public let icon: Icon + public let description: Description + public let leadingAccessory: LeadingAccessory + public let trailingAccessory: TrailingAccessory + public let isRangeSlider: Bool + public let valueFormat: String? + public let rangeFormat: (String, String)? + public let leadingValueFormat: String? + public let trailingValueFormat: String? + public let showsValueLabel: Bool + public let showsLeadingAccessory: Bool + public let showsTrailingAccessory: Bool + public let onValueChange: ((Bool, Double) -> Void)? + public let onEditFieldFocusStatusChange: ((Bool) -> Void)? + + public typealias Title = ConfigurationViewWrapper + public typealias ValueLabel = ConfigurationViewWrapper + public typealias LowerThumb = ConfigurationViewWrapper + public typealias UpperThumb = ConfigurationViewWrapper + public typealias ActiveTrack = ConfigurationViewWrapper + public typealias InactiveTrack = ConfigurationViewWrapper + public typealias Icon = ConfigurationViewWrapper + public typealias Description = ConfigurationViewWrapper + public typealias LeadingAccessory = ConfigurationViewWrapper + public typealias TrailingAccessory = ConfigurationViewWrapper +} + +public struct FioriSliderFioriStyle: FioriSliderStyle { + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .titleStyle(TitleFioriStyle(fioriSliderConfiguration: configuration)) + .valueLabelStyle(ValueLabelFioriStyle(fioriSliderConfiguration: configuration)) + .lowerThumbStyle(LowerThumbFioriStyle(fioriSliderConfiguration: configuration)) + .upperThumbStyle(UpperThumbFioriStyle(fioriSliderConfiguration: configuration)) + .activeTrackStyle(ActiveTrackFioriStyle(fioriSliderConfiguration: configuration)) + .inactiveTrackStyle(InactiveTrackFioriStyle(fioriSliderConfiguration: configuration)) + .iconStyle(IconFioriStyle(fioriSliderConfiguration: configuration)) + .descriptionStyle(DescriptionFioriStyle(fioriSliderConfiguration: configuration)) + .leadingAccessoryStyle(LeadingAccessoryFioriStyle(fioriSliderConfiguration: configuration)) + .trailingAccessoryStyle(TrailingAccessoryFioriStyle(fioriSliderConfiguration: configuration)) + .rangeSliderControlStyle(RangeSliderControlFioriStyle(fioriSliderConfiguration: configuration)) + .informationViewStyle(InformationViewFioriStyle(fioriSliderConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrack.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrack.generated.swift new file mode 100644 index 000000000..17c3d99ce --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrack.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct InactiveTrack { + let inactiveTrack: any View + + @Environment(\.inactiveTrackStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder inactiveTrack: () -> any View) { + self.inactiveTrack = inactiveTrack() + } +} + +public extension InactiveTrack { + init(inactiveTrack: any Shape = Capsule()) { + self.init(inactiveTrack: { inactiveTrack }) + } +} + +public extension InactiveTrack { + init(_ configuration: InactiveTrackConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: InactiveTrackConfiguration, shouldApplyDefaultStyle: Bool) { + self.inactiveTrack = configuration.inactiveTrack + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension InactiveTrack: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(inactiveTrack: .init(self.inactiveTrack))).typeErased + .transformEnvironment(\.inactiveTrackStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension InactiveTrack { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + InactiveTrack(.init(inactiveTrack: .init(self.inactiveTrack))) + .shouldApplyDefaultStyle(false) + .inactiveTrackStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrackStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrackStyle.generated.swift new file mode 100644 index 000000000..b410dc913 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/InactiveTrack/InactiveTrackStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol InactiveTrackStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: InactiveTrackConfiguration) -> Body +} + +struct AnyInactiveTrackStyle: InactiveTrackStyle { + let content: (InactiveTrackConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (InactiveTrackConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct InactiveTrackConfiguration { + public let inactiveTrack: InactiveTrack + + public typealias InactiveTrack = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessory.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessory.generated.swift new file mode 100644 index 000000000..455d7dfdc --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessory.generated.swift @@ -0,0 +1,57 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct LeadingAccessory { + let leadingAccessory: any View + + @Environment(\.leadingAccessoryStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder leadingAccessory: () -> any View = { EmptyView() }) { + self.leadingAccessory = leadingAccessory() + } +} + +public extension LeadingAccessory { + init(_ configuration: LeadingAccessoryConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: LeadingAccessoryConfiguration, shouldApplyDefaultStyle: Bool) { + self.leadingAccessory = configuration.leadingAccessory + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension LeadingAccessory: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(leadingAccessory: .init(self.leadingAccessory))).typeErased + .transformEnvironment(\.leadingAccessoryStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension LeadingAccessory { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + LeadingAccessory(.init(leadingAccessory: .init(self.leadingAccessory))) + .shouldApplyDefaultStyle(false) + .leadingAccessoryStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessoryStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessoryStyle.generated.swift new file mode 100644 index 000000000..11ed58f61 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LeadingAccessory/LeadingAccessoryStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol LeadingAccessoryStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: LeadingAccessoryConfiguration) -> Body +} + +struct AnyLeadingAccessoryStyle: LeadingAccessoryStyle { + let content: (LeadingAccessoryConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (LeadingAccessoryConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: LeadingAccessoryConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct LeadingAccessoryConfiguration { + public let leadingAccessory: LeadingAccessory + + public typealias LeadingAccessory = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumb.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumb.generated.swift new file mode 100644 index 000000000..86118ce2b --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumb.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct LowerThumb { + let lowerThumb: any View + + @Environment(\.lowerThumbStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder lowerThumb: () -> any View) { + self.lowerThumb = lowerThumb() + } +} + +public extension LowerThumb { + init(lowerThumb: any Shape = Circle()) { + self.init(lowerThumb: { lowerThumb }) + } +} + +public extension LowerThumb { + init(_ configuration: LowerThumbConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: LowerThumbConfiguration, shouldApplyDefaultStyle: Bool) { + self.lowerThumb = configuration.lowerThumb + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension LowerThumb: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(lowerThumb: .init(self.lowerThumb))).typeErased + .transformEnvironment(\.lowerThumbStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension LowerThumb { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + LowerThumb(.init(lowerThumb: .init(self.lowerThumb))) + .shouldApplyDefaultStyle(false) + .lowerThumbStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumbStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumbStyle.generated.swift new file mode 100644 index 000000000..d1bd27398 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/LowerThumb/LowerThumbStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol LowerThumbStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: LowerThumbConfiguration) -> Body +} + +struct AnyLowerThumbStyle: LowerThumbStyle { + let content: (LowerThumbConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (LowerThumbConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct LowerThumbConfiguration { + public let lowerThumb: LowerThumb + + public typealias LowerThumb = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControl.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControl.generated.swift new file mode 100644 index 000000000..15a44d69d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControl.generated.swift @@ -0,0 +1,134 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct RangeSliderControl { + let lowerThumb: any View + let upperThumb: any View + let activeTrack: any View + let inactiveTrack: any View + /// The lower value of range slider. + @Binding var lowerValue: Double + /// The upper value of range slider + @Binding var upperValue: Double + /// The range of the slider values. The default is `0...100`. + let range: ClosedRange + /// The distance between each valid value. The default is `1` + let step: Double + /// This property specifies the number of digits that should appear after the decimal point in the Double value for slider value or lower/upper value for range slider . It controls the precision of the numerical representation by determining how many decimal places are displayed or used in calculations, rounding the Double accordingly. The default is `0` + let decimalPlaces: Int + /// The half-width of the thumb. This value only takes effect for a range slider. In the context of a circular representation of the thumb, this value is used as the radius. It should be less than 22. The default value is `14`. + let thumbHalfWidth: CGFloat + /// Indicates whether the lower thumb is to be displayed or not. The default value is `true` + let showsLowerThumb: Bool + /// Indicates whether the upper thumb is to be displayed or not. The default value is `true` + let showsUpperThumb: Bool + /// An optional callback function that is triggered when the user begins to drag either the lower or upper thumb along the range slider's track or edit the value in text field to adjust the slider's values. The first boolean parameter indicates whether the editing has begun or ended, with `false` signifying that the editing has ended. The second parameter is a double that represents the updated lower value, while the third parameter (also a double) represents the updated upper value. + let onRangeValueChange: ((Bool, Double, Double) -> Void)? + + @Environment(\.rangeSliderControlStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder lowerThumb: () -> any View, + @ViewBuilder upperThumb: () -> any View, + @ViewBuilder activeTrack: () -> any View, + @ViewBuilder inactiveTrack: () -> any View, + lowerValue: Binding, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + thumbHalfWidth: CGFloat = 14, + showsLowerThumb: Bool = true, + showsUpperThumb: Bool = true, + onRangeValueChange: ((Bool, Double, Double) -> Void)? = nil) + { + self.lowerThumb = LowerThumb(lowerThumb: lowerThumb) + self.upperThumb = UpperThumb(upperThumb: upperThumb) + self.activeTrack = ActiveTrack(activeTrack: activeTrack) + self.inactiveTrack = InactiveTrack(inactiveTrack: inactiveTrack) + self._lowerValue = lowerValue + self._upperValue = upperValue + self.range = range + self.step = step + self.decimalPlaces = decimalPlaces + self.thumbHalfWidth = thumbHalfWidth + self.showsLowerThumb = showsLowerThumb + self.showsUpperThumb = showsUpperThumb + self.onRangeValueChange = onRangeValueChange + } +} + +public extension RangeSliderControl { + init(lowerThumb: any Shape = Circle(), + upperThumb: any Shape = Circle(), + activeTrack: any Shape = Capsule(), + inactiveTrack: any Shape = Capsule(), + lowerValue: Binding, + upperValue: Binding, + range: ClosedRange = 0 ... 100, + step: Double = 1, + decimalPlaces: Int = 0, + thumbHalfWidth: CGFloat = 14, + showsLowerThumb: Bool = true, + showsUpperThumb: Bool = true, + onRangeValueChange: ((Bool, Double, Double) -> Void)? = nil) + { + self.init(lowerThumb: { lowerThumb }, upperThumb: { upperThumb }, activeTrack: { activeTrack }, inactiveTrack: { inactiveTrack }, lowerValue: lowerValue, upperValue: upperValue, range: range, step: step, decimalPlaces: decimalPlaces, thumbHalfWidth: thumbHalfWidth, showsLowerThumb: showsLowerThumb, showsUpperThumb: showsUpperThumb, onRangeValueChange: onRangeValueChange) + } +} + +public extension RangeSliderControl { + init(_ configuration: RangeSliderControlConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: RangeSliderControlConfiguration, shouldApplyDefaultStyle: Bool) { + self.lowerThumb = configuration.lowerThumb + self.upperThumb = configuration.upperThumb + self.activeTrack = configuration.activeTrack + self.inactiveTrack = configuration.inactiveTrack + self._lowerValue = configuration.$lowerValue + self._upperValue = configuration.$upperValue + self.range = configuration.range + self.step = configuration.step + self.decimalPlaces = configuration.decimalPlaces + self.thumbHalfWidth = configuration.thumbHalfWidth + self.showsLowerThumb = configuration.showsLowerThumb + self.showsUpperThumb = configuration.showsUpperThumb + self.onRangeValueChange = configuration.onRangeValueChange + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension RangeSliderControl: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(lowerThumb: .init(self.lowerThumb), upperThumb: .init(self.upperThumb), activeTrack: .init(self.activeTrack), inactiveTrack: .init(self.inactiveTrack), lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: self.onRangeValueChange)).typeErased + .transformEnvironment(\.rangeSliderControlStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension RangeSliderControl { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + RangeSliderControl(.init(lowerThumb: .init(self.lowerThumb), upperThumb: .init(self.upperThumb), activeTrack: .init(self.activeTrack), inactiveTrack: .init(self.inactiveTrack), lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: self.onRangeValueChange)) + .shouldApplyDefaultStyle(false) + .rangeSliderControlStyle(RangeSliderControlFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControlStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControlStyle.generated.swift new file mode 100644 index 000000000..c4e92133d --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/RangeSliderControl/RangeSliderControlStyle.generated.swift @@ -0,0 +1,53 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol RangeSliderControlStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: RangeSliderControlConfiguration) -> Body +} + +struct AnyRangeSliderControlStyle: RangeSliderControlStyle { + let content: (RangeSliderControlConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (RangeSliderControlConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct RangeSliderControlConfiguration { + public let lowerThumb: LowerThumb + public let upperThumb: UpperThumb + public let activeTrack: ActiveTrack + public let inactiveTrack: InactiveTrack + @Binding public var lowerValue: Double + @Binding public var upperValue: Double + public let range: ClosedRange + public let step: Double + public let decimalPlaces: Int + public let thumbHalfWidth: CGFloat + public let showsLowerThumb: Bool + public let showsUpperThumb: Bool + public let onRangeValueChange: ((Bool, Double, Double) -> Void)? + + public typealias LowerThumb = ConfigurationViewWrapper + public typealias UpperThumb = ConfigurationViewWrapper + public typealias ActiveTrack = ConfigurationViewWrapper + public typealias InactiveTrack = ConfigurationViewWrapper +} + +public struct RangeSliderControlFioriStyle: RangeSliderControlStyle { + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .lowerThumbStyle(LowerThumbFioriStyle(rangeSliderControlConfiguration: configuration)) + .upperThumbStyle(UpperThumbFioriStyle(rangeSliderControlConfiguration: configuration)) + .activeTrackStyle(ActiveTrackFioriStyle(rangeSliderControlConfiguration: configuration)) + .inactiveTrackStyle(InactiveTrackFioriStyle(rangeSliderControlConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessory.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessory.generated.swift new file mode 100644 index 000000000..c9444e9b4 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessory.generated.swift @@ -0,0 +1,57 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct TrailingAccessory { + let trailingAccessory: any View + + @Environment(\.trailingAccessoryStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder trailingAccessory: () -> any View = { EmptyView() }) { + self.trailingAccessory = trailingAccessory() + } +} + +public extension TrailingAccessory { + init(_ configuration: TrailingAccessoryConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: TrailingAccessoryConfiguration, shouldApplyDefaultStyle: Bool) { + self.trailingAccessory = configuration.trailingAccessory + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension TrailingAccessory: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(trailingAccessory: .init(self.trailingAccessory))).typeErased + .transformEnvironment(\.trailingAccessoryStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension TrailingAccessory { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + TrailingAccessory(.init(trailingAccessory: .init(self.trailingAccessory))) + .shouldApplyDefaultStyle(false) + .trailingAccessoryStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessoryStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessoryStyle.generated.swift new file mode 100644 index 000000000..8b256b496 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/TrailingAccessory/TrailingAccessoryStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol TrailingAccessoryStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: TrailingAccessoryConfiguration) -> Body +} + +struct AnyTrailingAccessoryStyle: TrailingAccessoryStyle { + let content: (TrailingAccessoryConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (TrailingAccessoryConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: TrailingAccessoryConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct TrailingAccessoryConfiguration { + public let trailingAccessory: TrailingAccessory + + public typealias TrailingAccessory = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumb.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumb.generated.swift new file mode 100644 index 000000000..d811bed12 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumb.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct UpperThumb { + let upperThumb: any View + + @Environment(\.upperThumbStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder upperThumb: () -> any View) { + self.upperThumb = upperThumb() + } +} + +public extension UpperThumb { + init(upperThumb: any Shape = Circle()) { + self.init(upperThumb: { upperThumb }) + } +} + +public extension UpperThumb { + init(_ configuration: UpperThumbConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: UpperThumbConfiguration, shouldApplyDefaultStyle: Bool) { + self.upperThumb = configuration.upperThumb + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension UpperThumb: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(upperThumb: .init(self.upperThumb))).typeErased + .transformEnvironment(\.upperThumbStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension UpperThumb { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + UpperThumb(.init(upperThumb: .init(self.upperThumb))) + .shouldApplyDefaultStyle(false) + .upperThumbStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumbStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumbStyle.generated.swift new file mode 100644 index 000000000..fd3573f19 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/UpperThumb/UpperThumbStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol UpperThumbStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: UpperThumbConfiguration) -> Body +} + +struct AnyUpperThumbStyle: UpperThumbStyle { + let content: (UpperThumbConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (UpperThumbConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct UpperThumbConfiguration { + public let upperThumb: UpperThumb + + public typealias UpperThumb = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index e247b5ed0..ac73e41f2 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -31,6 +31,20 @@ public extension ActionStyle where Self == ActionFioriStyle { } } +// MARK: ActiveTrackStyle + +public extension ActiveTrackStyle where Self == ActiveTrackBaseStyle { + static var base: ActiveTrackBaseStyle { + ActiveTrackBaseStyle() + } +} + +public extension ActiveTrackStyle where Self == ActiveTrackFioriStyle { + static var fiori: ActiveTrackFioriStyle { + ActiveTrackFioriStyle() + } +} + // MARK: ActivityItemStyle public extension ActivityItemStyle where Self == ActivityItemBaseStyle { @@ -1921,6 +1935,272 @@ public extension FilledIconStyle where Self == FilledIconFioriStyle { } } +// MARK: FioriSliderStyle + +public extension FioriSliderStyle where Self == FioriSliderBaseStyle { + static var base: FioriSliderBaseStyle { + FioriSliderBaseStyle() + } +} + +public extension FioriSliderStyle where Self == FioriSliderFioriStyle { + static var fiori: FioriSliderFioriStyle { + FioriSliderFioriStyle() + } +} + +public struct FioriSliderTitleStyle: FioriSliderStyle { + let style: any TitleStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .titleStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderTitleStyle { + static func titleStyle(_ style: some TitleStyle) -> FioriSliderTitleStyle { + FioriSliderTitleStyle(style: style) + } + + static func titleStyle(@ViewBuilder content: @escaping (TitleConfiguration) -> some View) -> FioriSliderTitleStyle { + let style = AnyTitleStyle(content) + return FioriSliderTitleStyle(style: style) + } +} + +public struct FioriSliderValueLabelStyle: FioriSliderStyle { + let style: any ValueLabelStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .valueLabelStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderValueLabelStyle { + static func valueLabelStyle(_ style: some ValueLabelStyle) -> FioriSliderValueLabelStyle { + FioriSliderValueLabelStyle(style: style) + } + + static func valueLabelStyle(@ViewBuilder content: @escaping (ValueLabelConfiguration) -> some View) -> FioriSliderValueLabelStyle { + let style = AnyValueLabelStyle(content) + return FioriSliderValueLabelStyle(style: style) + } +} + +public struct FioriSliderLowerThumbStyle: FioriSliderStyle { + let style: any LowerThumbStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .lowerThumbStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderLowerThumbStyle { + static func lowerThumbStyle(_ style: some LowerThumbStyle) -> FioriSliderLowerThumbStyle { + FioriSliderLowerThumbStyle(style: style) + } + + static func lowerThumbStyle(@ViewBuilder content: @escaping (LowerThumbConfiguration) -> some View) -> FioriSliderLowerThumbStyle { + let style = AnyLowerThumbStyle(content) + return FioriSliderLowerThumbStyle(style: style) + } +} + +public struct FioriSliderUpperThumbStyle: FioriSliderStyle { + let style: any UpperThumbStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .upperThumbStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderUpperThumbStyle { + static func upperThumbStyle(_ style: some UpperThumbStyle) -> FioriSliderUpperThumbStyle { + FioriSliderUpperThumbStyle(style: style) + } + + static func upperThumbStyle(@ViewBuilder content: @escaping (UpperThumbConfiguration) -> some View) -> FioriSliderUpperThumbStyle { + let style = AnyUpperThumbStyle(content) + return FioriSliderUpperThumbStyle(style: style) + } +} + +public struct FioriSliderActiveTrackStyle: FioriSliderStyle { + let style: any ActiveTrackStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .activeTrackStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderActiveTrackStyle { + static func activeTrackStyle(_ style: some ActiveTrackStyle) -> FioriSliderActiveTrackStyle { + FioriSliderActiveTrackStyle(style: style) + } + + static func activeTrackStyle(@ViewBuilder content: @escaping (ActiveTrackConfiguration) -> some View) -> FioriSliderActiveTrackStyle { + let style = AnyActiveTrackStyle(content) + return FioriSliderActiveTrackStyle(style: style) + } +} + +public struct FioriSliderInactiveTrackStyle: FioriSliderStyle { + let style: any InactiveTrackStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .inactiveTrackStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderInactiveTrackStyle { + static func inactiveTrackStyle(_ style: some InactiveTrackStyle) -> FioriSliderInactiveTrackStyle { + FioriSliderInactiveTrackStyle(style: style) + } + + static func inactiveTrackStyle(@ViewBuilder content: @escaping (InactiveTrackConfiguration) -> some View) -> FioriSliderInactiveTrackStyle { + let style = AnyInactiveTrackStyle(content) + return FioriSliderInactiveTrackStyle(style: style) + } +} + +public struct FioriSliderIconStyle: FioriSliderStyle { + let style: any IconStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .iconStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderIconStyle { + static func iconStyle(_ style: some IconStyle) -> FioriSliderIconStyle { + FioriSliderIconStyle(style: style) + } + + static func iconStyle(@ViewBuilder content: @escaping (IconConfiguration) -> some View) -> FioriSliderIconStyle { + let style = AnyIconStyle(content) + return FioriSliderIconStyle(style: style) + } +} + +public struct FioriSliderDescriptionStyle: FioriSliderStyle { + let style: any DescriptionStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .descriptionStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderDescriptionStyle { + static func descriptionStyle(_ style: some DescriptionStyle) -> FioriSliderDescriptionStyle { + FioriSliderDescriptionStyle(style: style) + } + + static func descriptionStyle(@ViewBuilder content: @escaping (DescriptionConfiguration) -> some View) -> FioriSliderDescriptionStyle { + let style = AnyDescriptionStyle(content) + return FioriSliderDescriptionStyle(style: style) + } +} + +public struct FioriSliderLeadingAccessoryStyle: FioriSliderStyle { + let style: any LeadingAccessoryStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .leadingAccessoryStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderLeadingAccessoryStyle { + static func leadingAccessoryStyle(_ style: some LeadingAccessoryStyle) -> FioriSliderLeadingAccessoryStyle { + FioriSliderLeadingAccessoryStyle(style: style) + } + + static func leadingAccessoryStyle(@ViewBuilder content: @escaping (LeadingAccessoryConfiguration) -> some View) -> FioriSliderLeadingAccessoryStyle { + let style = AnyLeadingAccessoryStyle(content) + return FioriSliderLeadingAccessoryStyle(style: style) + } +} + +public struct FioriSliderTrailingAccessoryStyle: FioriSliderStyle { + let style: any TrailingAccessoryStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .trailingAccessoryStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderTrailingAccessoryStyle { + static func trailingAccessoryStyle(_ style: some TrailingAccessoryStyle) -> FioriSliderTrailingAccessoryStyle { + FioriSliderTrailingAccessoryStyle(style: style) + } + + static func trailingAccessoryStyle(@ViewBuilder content: @escaping (TrailingAccessoryConfiguration) -> some View) -> FioriSliderTrailingAccessoryStyle { + let style = AnyTrailingAccessoryStyle(content) + return FioriSliderTrailingAccessoryStyle(style: style) + } +} + +public struct FioriSliderRangeSliderControlStyle: FioriSliderStyle { + let style: any RangeSliderControlStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .rangeSliderControlStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderRangeSliderControlStyle { + static func rangeSliderControlStyle(_ style: some RangeSliderControlStyle) -> FioriSliderRangeSliderControlStyle { + FioriSliderRangeSliderControlStyle(style: style) + } + + static func rangeSliderControlStyle(@ViewBuilder content: @escaping (RangeSliderControlConfiguration) -> some View) -> FioriSliderRangeSliderControlStyle { + let style = AnyRangeSliderControlStyle(content) + return FioriSliderRangeSliderControlStyle(style: style) + } +} + +public struct FioriSliderInformationViewStyle: FioriSliderStyle { + let style: any InformationViewStyle + + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .informationViewStyle(self.style) + .typeErased + } +} + +public extension FioriSliderStyle where Self == FioriSliderInformationViewStyle { + static func informationViewStyle(_ style: some InformationViewStyle) -> FioriSliderInformationViewStyle { + FioriSliderInformationViewStyle(style: style) + } + + static func informationViewStyle(@ViewBuilder content: @escaping (InformationViewConfiguration) -> some View) -> FioriSliderInformationViewStyle { + let style = AnyInformationViewStyle(content) + return FioriSliderInformationViewStyle(style: style) + } +} + // MARK: FootnoteStyle public extension FootnoteStyle where Self == FootnoteBaseStyle { @@ -2180,6 +2460,20 @@ public extension IllustratedMessageStyle where Self == IllustratedMessageSeconda } } +// MARK: InactiveTrackStyle + +public extension InactiveTrackStyle where Self == InactiveTrackBaseStyle { + static var base: InactiveTrackBaseStyle { + InactiveTrackBaseStyle() + } +} + +public extension InactiveTrackStyle where Self == InactiveTrackFioriStyle { + static var fiori: InactiveTrackFioriStyle { + InactiveTrackFioriStyle() + } +} + // MARK: IncrementActionStyle public extension IncrementActionStyle where Self == IncrementActionBaseStyle { @@ -2572,6 +2866,20 @@ public extension LabelItemStyle where Self == LabelItemTitleStyle { } } +// MARK: LeadingAccessoryStyle + +public extension LeadingAccessoryStyle where Self == LeadingAccessoryBaseStyle { + static var base: LeadingAccessoryBaseStyle { + LeadingAccessoryBaseStyle() + } +} + +public extension LeadingAccessoryStyle where Self == LeadingAccessoryFioriStyle { + static var fiori: LeadingAccessoryFioriStyle { + LeadingAccessoryFioriStyle() + } +} + // MARK: LinearProgressIndicatorStyle public extension LinearProgressIndicatorStyle where Self == LinearProgressIndicatorBaseStyle { @@ -2992,6 +3300,20 @@ public extension LoadingIndicatorStyle where Self == LoadingIndicatorProgressSty } } +// MARK: LowerThumbStyle + +public extension LowerThumbStyle where Self == LowerThumbBaseStyle { + static var base: LowerThumbBaseStyle { + LowerThumbBaseStyle() + } +} + +public extension LowerThumbStyle where Self == LowerThumbFioriStyle { + static var fiori: LowerThumbFioriStyle { + LowerThumbFioriStyle() + } +} + // MARK: MandatoryFieldIndicatorStyle public extension MandatoryFieldIndicatorStyle where Self == MandatoryFieldIndicatorBaseStyle { @@ -3930,6 +4252,104 @@ public extension ProgressIndicatorProtocolStyle where Self == ProgressIndicatorP } } +// MARK: RangeSliderControlStyle + +public extension RangeSliderControlStyle where Self == RangeSliderControlBaseStyle { + static var base: RangeSliderControlBaseStyle { + RangeSliderControlBaseStyle() + } +} + +public extension RangeSliderControlStyle where Self == RangeSliderControlFioriStyle { + static var fiori: RangeSliderControlFioriStyle { + RangeSliderControlFioriStyle() + } +} + +public struct RangeSliderControlLowerThumbStyle: RangeSliderControlStyle { + let style: any LowerThumbStyle + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .lowerThumbStyle(self.style) + .typeErased + } +} + +public extension RangeSliderControlStyle where Self == RangeSliderControlLowerThumbStyle { + static func lowerThumbStyle(_ style: some LowerThumbStyle) -> RangeSliderControlLowerThumbStyle { + RangeSliderControlLowerThumbStyle(style: style) + } + + static func lowerThumbStyle(@ViewBuilder content: @escaping (LowerThumbConfiguration) -> some View) -> RangeSliderControlLowerThumbStyle { + let style = AnyLowerThumbStyle(content) + return RangeSliderControlLowerThumbStyle(style: style) + } +} + +public struct RangeSliderControlUpperThumbStyle: RangeSliderControlStyle { + let style: any UpperThumbStyle + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .upperThumbStyle(self.style) + .typeErased + } +} + +public extension RangeSliderControlStyle where Self == RangeSliderControlUpperThumbStyle { + static func upperThumbStyle(_ style: some UpperThumbStyle) -> RangeSliderControlUpperThumbStyle { + RangeSliderControlUpperThumbStyle(style: style) + } + + static func upperThumbStyle(@ViewBuilder content: @escaping (UpperThumbConfiguration) -> some View) -> RangeSliderControlUpperThumbStyle { + let style = AnyUpperThumbStyle(content) + return RangeSliderControlUpperThumbStyle(style: style) + } +} + +public struct RangeSliderControlActiveTrackStyle: RangeSliderControlStyle { + let style: any ActiveTrackStyle + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .activeTrackStyle(self.style) + .typeErased + } +} + +public extension RangeSliderControlStyle where Self == RangeSliderControlActiveTrackStyle { + static func activeTrackStyle(_ style: some ActiveTrackStyle) -> RangeSliderControlActiveTrackStyle { + RangeSliderControlActiveTrackStyle(style: style) + } + + static func activeTrackStyle(@ViewBuilder content: @escaping (ActiveTrackConfiguration) -> some View) -> RangeSliderControlActiveTrackStyle { + let style = AnyActiveTrackStyle(content) + return RangeSliderControlActiveTrackStyle(style: style) + } +} + +public struct RangeSliderControlInactiveTrackStyle: RangeSliderControlStyle { + let style: any InactiveTrackStyle + + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .inactiveTrackStyle(self.style) + .typeErased + } +} + +public extension RangeSliderControlStyle where Self == RangeSliderControlInactiveTrackStyle { + static func inactiveTrackStyle(_ style: some InactiveTrackStyle) -> RangeSliderControlInactiveTrackStyle { + RangeSliderControlInactiveTrackStyle(style: style) + } + + static func inactiveTrackStyle(@ViewBuilder content: @escaping (InactiveTrackConfiguration) -> some View) -> RangeSliderControlInactiveTrackStyle { + let style = AnyInactiveTrackStyle(content) + return RangeSliderControlInactiveTrackStyle(style: style) + } +} + // MARK: RatingControlStyle public extension RatingControlStyle where Self == RatingControlBaseStyle { @@ -5946,6 +6366,34 @@ public extension TopDividerStyle where Self == TopDividerFioriStyle { } } +// MARK: TrailingAccessoryStyle + +public extension TrailingAccessoryStyle where Self == TrailingAccessoryBaseStyle { + static var base: TrailingAccessoryBaseStyle { + TrailingAccessoryBaseStyle() + } +} + +public extension TrailingAccessoryStyle where Self == TrailingAccessoryFioriStyle { + static var fiori: TrailingAccessoryFioriStyle { + TrailingAccessoryFioriStyle() + } +} + +// MARK: UpperThumbStyle + +public extension UpperThumbStyle where Self == UpperThumbBaseStyle { + static var base: UpperThumbBaseStyle { + UpperThumbBaseStyle() + } +} + +public extension UpperThumbStyle where Self == UpperThumbFioriStyle { + static var fiori: UpperThumbFioriStyle { + UpperThumbFioriStyle() + } +} + // MARK: ValueStyle public extension ValueStyle where Self == ValueBaseStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index afe031c81..8bca72424 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -45,6 +45,27 @@ extension EnvironmentValues { } } +// MARK: ActiveTrackStyle + +struct ActiveTrackStyleStackKey: EnvironmentKey { + static let defaultValue: [any ActiveTrackStyle] = [] +} + +extension EnvironmentValues { + var activeTrackStyle: any ActiveTrackStyle { + self.activeTrackStyleStack.last ?? .base + } + + var activeTrackStyleStack: [any ActiveTrackStyle] { + get { + self[ActiveTrackStyleStackKey.self] + } + set { + self[ActiveTrackStyleStackKey.self] = newValue + } + } +} + // MARK: ActivityItemStyle struct ActivityItemStyleStackKey: EnvironmentKey { @@ -612,6 +633,27 @@ extension EnvironmentValues { } } +// MARK: FioriSliderStyle + +struct FioriSliderStyleStackKey: EnvironmentKey { + static let defaultValue: [any FioriSliderStyle] = [] +} + +extension EnvironmentValues { + var fioriSliderStyle: any FioriSliderStyle { + self.fioriSliderStyleStack.last ?? .base.concat(.fiori) + } + + var fioriSliderStyleStack: [any FioriSliderStyle] { + get { + self[FioriSliderStyleStackKey.self] + } + set { + self[FioriSliderStyleStackKey.self] = newValue + } + } +} + // MARK: FootnoteStyle struct FootnoteStyleStackKey: EnvironmentKey { @@ -843,6 +885,27 @@ extension EnvironmentValues { } } +// MARK: InactiveTrackStyle + +struct InactiveTrackStyleStackKey: EnvironmentKey { + static let defaultValue: [any InactiveTrackStyle] = [] +} + +extension EnvironmentValues { + var inactiveTrackStyle: any InactiveTrackStyle { + self.inactiveTrackStyleStack.last ?? .base + } + + var inactiveTrackStyleStack: [any InactiveTrackStyle] { + get { + self[InactiveTrackStyleStackKey.self] + } + set { + self[InactiveTrackStyleStackKey.self] = newValue + } + } +} + // MARK: IncrementActionStyle struct IncrementActionStyleStackKey: EnvironmentKey { @@ -990,6 +1053,27 @@ extension EnvironmentValues { } } +// MARK: LeadingAccessoryStyle + +struct LeadingAccessoryStyleStackKey: EnvironmentKey { + static let defaultValue: [any LeadingAccessoryStyle] = [] +} + +extension EnvironmentValues { + var leadingAccessoryStyle: any LeadingAccessoryStyle { + self.leadingAccessoryStyleStack.last ?? .base + } + + var leadingAccessoryStyleStack: [any LeadingAccessoryStyle] { + get { + self[LeadingAccessoryStyleStackKey.self] + } + set { + self[LeadingAccessoryStyleStackKey.self] = newValue + } + } +} + // MARK: LinearProgressIndicatorStyle struct LinearProgressIndicatorStyleStackKey: EnvironmentKey { @@ -1116,6 +1200,27 @@ extension EnvironmentValues { } } +// MARK: LowerThumbStyle + +struct LowerThumbStyleStackKey: EnvironmentKey { + static let defaultValue: [any LowerThumbStyle] = [] +} + +extension EnvironmentValues { + var lowerThumbStyle: any LowerThumbStyle { + self.lowerThumbStyleStack.last ?? .base + } + + var lowerThumbStyleStack: [any LowerThumbStyle] { + get { + self[LowerThumbStyleStackKey.self] + } + set { + self[LowerThumbStyleStackKey.self] = newValue + } + } +} + // MARK: MandatoryFieldIndicatorStyle struct MandatoryFieldIndicatorStyleStackKey: EnvironmentKey { @@ -1578,6 +1683,27 @@ extension EnvironmentValues { } } +// MARK: RangeSliderControlStyle + +struct RangeSliderControlStyleStackKey: EnvironmentKey { + static let defaultValue: [any RangeSliderControlStyle] = [] +} + +extension EnvironmentValues { + var rangeSliderControlStyle: any RangeSliderControlStyle { + self.rangeSliderControlStyleStack.last ?? .base.concat(.fiori) + } + + var rangeSliderControlStyleStack: [any RangeSliderControlStyle] { + get { + self[RangeSliderControlStyleStackKey.self] + } + set { + self[RangeSliderControlStyleStackKey.self] = newValue + } + } +} + // MARK: RatingControlStyle struct RatingControlStyleStackKey: EnvironmentKey { @@ -2397,6 +2523,48 @@ extension EnvironmentValues { } } +// MARK: TrailingAccessoryStyle + +struct TrailingAccessoryStyleStackKey: EnvironmentKey { + static let defaultValue: [any TrailingAccessoryStyle] = [] +} + +extension EnvironmentValues { + var trailingAccessoryStyle: any TrailingAccessoryStyle { + self.trailingAccessoryStyleStack.last ?? .base + } + + var trailingAccessoryStyleStack: [any TrailingAccessoryStyle] { + get { + self[TrailingAccessoryStyleStackKey.self] + } + set { + self[TrailingAccessoryStyleStackKey.self] = newValue + } + } +} + +// MARK: UpperThumbStyle + +struct UpperThumbStyleStackKey: EnvironmentKey { + static let defaultValue: [any UpperThumbStyle] = [] +} + +extension EnvironmentValues { + var upperThumbStyle: any UpperThumbStyle { + self.upperThumbStyleStack.last ?? .base + } + + var upperThumbStyleStack: [any UpperThumbStyle] { + get { + self[UpperThumbStyleStackKey.self] + } + set { + self[UpperThumbStyleStackKey.self] = newValue + } + } +} + // MARK: ValueStyle struct ValueStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index 393373a5d..7465a836c 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -64,6 +64,34 @@ public extension ActionStyle { } } +// MARK: ActiveTrackStyle + +extension ModifiedStyle: ActiveTrackStyle where Style: ActiveTrackStyle { + public func makeBody(_ configuration: ActiveTrackConfiguration) -> some View { + ActiveTrack(configuration) + .activeTrackStyle(self.style) + .modifier(self.modifier) + } +} + +public struct ActiveTrackStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.activeTrackStyle(self.style) + } +} + +public extension ActiveTrackStyle { + func modifier(_ modifier: some ViewModifier) -> some ActiveTrackStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some ActiveTrackStyle) -> some ActiveTrackStyle { + style.modifier(ActiveTrackStyleModifier(style: self)) + } +} + // MARK: ActivityItemStyle extension ModifiedStyle: ActivityItemStyle where Style: ActivityItemStyle { @@ -820,6 +848,34 @@ public extension FilledIconStyle { } } +// MARK: FioriSliderStyle + +extension ModifiedStyle: FioriSliderStyle where Style: FioriSliderStyle { + public func makeBody(_ configuration: FioriSliderConfiguration) -> some View { + FioriSlider(configuration) + .fioriSliderStyle(self.style) + .modifier(self.modifier) + } +} + +public struct FioriSliderStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.fioriSliderStyle(self.style) + } +} + +public extension FioriSliderStyle { + func modifier(_ modifier: some ViewModifier) -> some FioriSliderStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some FioriSliderStyle) -> some FioriSliderStyle { + style.modifier(FioriSliderStyleModifier(style: self)) + } +} + // MARK: FootnoteStyle extension ModifiedStyle: FootnoteStyle where Style: FootnoteStyle { @@ -1128,6 +1184,34 @@ public extension IllustratedMessageStyle { } } +// MARK: InactiveTrackStyle + +extension ModifiedStyle: InactiveTrackStyle where Style: InactiveTrackStyle { + public func makeBody(_ configuration: InactiveTrackConfiguration) -> some View { + InactiveTrack(configuration) + .inactiveTrackStyle(self.style) + .modifier(self.modifier) + } +} + +public struct InactiveTrackStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.inactiveTrackStyle(self.style) + } +} + +public extension InactiveTrackStyle { + func modifier(_ modifier: some ViewModifier) -> some InactiveTrackStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some InactiveTrackStyle) -> some InactiveTrackStyle { + style.modifier(InactiveTrackStyleModifier(style: self)) + } +} + // MARK: IncrementActionStyle extension ModifiedStyle: IncrementActionStyle where Style: IncrementActionStyle { @@ -1324,6 +1408,34 @@ public extension LabelItemStyle { } } +// MARK: LeadingAccessoryStyle + +extension ModifiedStyle: LeadingAccessoryStyle where Style: LeadingAccessoryStyle { + public func makeBody(_ configuration: LeadingAccessoryConfiguration) -> some View { + LeadingAccessory(configuration) + .leadingAccessoryStyle(self.style) + .modifier(self.modifier) + } +} + +public struct LeadingAccessoryStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.leadingAccessoryStyle(self.style) + } +} + +public extension LeadingAccessoryStyle { + func modifier(_ modifier: some ViewModifier) -> some LeadingAccessoryStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some LeadingAccessoryStyle) -> some LeadingAccessoryStyle { + style.modifier(LeadingAccessoryStyleModifier(style: self)) + } +} + // MARK: LinearProgressIndicatorStyle extension ModifiedStyle: LinearProgressIndicatorStyle where Style: LinearProgressIndicatorStyle { @@ -1492,6 +1604,34 @@ public extension LoadingIndicatorStyle { } } +// MARK: LowerThumbStyle + +extension ModifiedStyle: LowerThumbStyle where Style: LowerThumbStyle { + public func makeBody(_ configuration: LowerThumbConfiguration) -> some View { + LowerThumb(configuration) + .lowerThumbStyle(self.style) + .modifier(self.modifier) + } +} + +public struct LowerThumbStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.lowerThumbStyle(self.style) + } +} + +public extension LowerThumbStyle { + func modifier(_ modifier: some ViewModifier) -> some LowerThumbStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some LowerThumbStyle) -> some LowerThumbStyle { + style.modifier(LowerThumbStyleModifier(style: self)) + } +} + // MARK: MandatoryFieldIndicatorStyle extension ModifiedStyle: MandatoryFieldIndicatorStyle where Style: MandatoryFieldIndicatorStyle { @@ -2108,6 +2248,34 @@ public extension ProgressIndicatorProtocolStyle { } } +// MARK: RangeSliderControlStyle + +extension ModifiedStyle: RangeSliderControlStyle where Style: RangeSliderControlStyle { + public func makeBody(_ configuration: RangeSliderControlConfiguration) -> some View { + RangeSliderControl(configuration) + .rangeSliderControlStyle(self.style) + .modifier(self.modifier) + } +} + +public struct RangeSliderControlStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.rangeSliderControlStyle(self.style) + } +} + +public extension RangeSliderControlStyle { + func modifier(_ modifier: some ViewModifier) -> some RangeSliderControlStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some RangeSliderControlStyle) -> some RangeSliderControlStyle { + style.modifier(RangeSliderControlStyleModifier(style: self)) + } +} + // MARK: RatingControlStyle extension ModifiedStyle: RatingControlStyle where Style: RatingControlStyle { @@ -3200,6 +3368,62 @@ public extension TopDividerStyle { } } +// MARK: TrailingAccessoryStyle + +extension ModifiedStyle: TrailingAccessoryStyle where Style: TrailingAccessoryStyle { + public func makeBody(_ configuration: TrailingAccessoryConfiguration) -> some View { + TrailingAccessory(configuration) + .trailingAccessoryStyle(self.style) + .modifier(self.modifier) + } +} + +public struct TrailingAccessoryStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.trailingAccessoryStyle(self.style) + } +} + +public extension TrailingAccessoryStyle { + func modifier(_ modifier: some ViewModifier) -> some TrailingAccessoryStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some TrailingAccessoryStyle) -> some TrailingAccessoryStyle { + style.modifier(TrailingAccessoryStyleModifier(style: self)) + } +} + +// MARK: UpperThumbStyle + +extension ModifiedStyle: UpperThumbStyle where Style: UpperThumbStyle { + public func makeBody(_ configuration: UpperThumbConfiguration) -> some View { + UpperThumb(configuration) + .upperThumbStyle(self.style) + .modifier(self.modifier) + } +} + +public struct UpperThumbStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.upperThumbStyle(self.style) + } +} + +public extension UpperThumbStyle { + func modifier(_ modifier: some ViewModifier) -> some UpperThumbStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some UpperThumbStyle) -> some UpperThumbStyle { + style.modifier(UpperThumbStyleModifier(style: self)) + } +} + // MARK: ValueStyle extension ModifiedStyle: ValueStyle where Style: ValueStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index 6a394ebdb..1b9e40960 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -35,6 +35,22 @@ extension ActionStyle { } } +// MARK: ActiveTrackStyle + +struct ResolvedActiveTrackStyle: View { + let style: Style + let configuration: ActiveTrackConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension ActiveTrackStyle { + func resolve(configuration: ActiveTrackConfiguration) -> some View { + ResolvedActiveTrackStyle(style: self, configuration: configuration) + } +} + // MARK: ActivityItemStyle struct ResolvedActivityItemStyle: View { @@ -467,6 +483,22 @@ extension FilledIconStyle { } } +// MARK: FioriSliderStyle + +struct ResolvedFioriSliderStyle: View { + let style: Style + let configuration: FioriSliderConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension FioriSliderStyle { + func resolve(configuration: FioriSliderConfiguration) -> some View { + ResolvedFioriSliderStyle(style: self, configuration: configuration) + } +} + // MARK: FootnoteStyle struct ResolvedFootnoteStyle: View { @@ -643,6 +675,22 @@ extension IllustratedMessageStyle { } } +// MARK: InactiveTrackStyle + +struct ResolvedInactiveTrackStyle: View { + let style: Style + let configuration: InactiveTrackConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension InactiveTrackStyle { + func resolve(configuration: InactiveTrackConfiguration) -> some View { + ResolvedInactiveTrackStyle(style: self, configuration: configuration) + } +} + // MARK: IncrementActionStyle struct ResolvedIncrementActionStyle: View { @@ -755,6 +803,22 @@ extension LabelItemStyle { } } +// MARK: LeadingAccessoryStyle + +struct ResolvedLeadingAccessoryStyle: View { + let style: Style + let configuration: LeadingAccessoryConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension LeadingAccessoryStyle { + func resolve(configuration: LeadingAccessoryConfiguration) -> some View { + ResolvedLeadingAccessoryStyle(style: self, configuration: configuration) + } +} + // MARK: LinearProgressIndicatorStyle struct ResolvedLinearProgressIndicatorStyle: View { @@ -851,6 +915,22 @@ extension LoadingIndicatorStyle { } } +// MARK: LowerThumbStyle + +struct ResolvedLowerThumbStyle: View { + let style: Style + let configuration: LowerThumbConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension LowerThumbStyle { + func resolve(configuration: LowerThumbConfiguration) -> some View { + ResolvedLowerThumbStyle(style: self, configuration: configuration) + } +} + // MARK: MandatoryFieldIndicatorStyle struct ResolvedMandatoryFieldIndicatorStyle: View { @@ -1203,6 +1283,22 @@ extension ProgressIndicatorProtocolStyle { } } +// MARK: RangeSliderControlStyle + +struct ResolvedRangeSliderControlStyle: View { + let style: Style + let configuration: RangeSliderControlConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension RangeSliderControlStyle { + func resolve(configuration: RangeSliderControlConfiguration) -> some View { + ResolvedRangeSliderControlStyle(style: self, configuration: configuration) + } +} + // MARK: RatingControlStyle struct ResolvedRatingControlStyle: View { @@ -1827,6 +1923,38 @@ extension TopDividerStyle { } } +// MARK: TrailingAccessoryStyle + +struct ResolvedTrailingAccessoryStyle: View { + let style: Style + let configuration: TrailingAccessoryConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension TrailingAccessoryStyle { + func resolve(configuration: TrailingAccessoryConfiguration) -> some View { + ResolvedTrailingAccessoryStyle(style: self, configuration: configuration) + } +} + +// MARK: UpperThumbStyle + +struct ResolvedUpperThumbStyle: View { + let style: Style + let configuration: UpperThumbConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension UpperThumbStyle { + func resolve(configuration: UpperThumbConfiguration) -> some View { + ResolvedUpperThumbStyle(style: self, configuration: configuration) + } +} + // MARK: ValueStyle struct ResolvedValueStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift index 528bf183c..5ed6a2cd3 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift @@ -49,6 +49,18 @@ extension DemoViewConfiguration { } } +// MARK: FioriSliderConfiguration + +extension FioriSliderConfiguration { + var _rangeSliderControl: RangeSliderControl { + RangeSliderControl(.init(lowerThumb: .init(self.lowerThumb), upperThumb: .init(self.upperThumb), activeTrack: .init(self.activeTrack), inactiveTrack: .init(self.inactiveTrack), lowerValue: self.$lowerValue, upperValue: self.$upperValue, range: self.range, step: self.step, decimalPlaces: self.decimalPlaces, thumbHalfWidth: self.thumbHalfWidth, showsLowerThumb: self.showsLowerThumb, showsUpperThumb: self.showsUpperThumb, onRangeValueChange: self.onRangeValueChange), shouldApplyDefaultStyle: true) + } + + var _informationView: InformationView { + InformationView(.init(icon: .init(self.icon), description: .init(self.description)), shouldApplyDefaultStyle: true) + } +} + // MARK: KeyValueFormViewConfiguration extension KeyValueFormViewConfiguration { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index 04f05b78e..ca0610239 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -37,6 +37,23 @@ public extension View { } } +// MARK: ActiveTrackStyle + +public extension View { + func activeTrackStyle(_ style: some ActiveTrackStyle) -> some View { + self.transformEnvironment(\.activeTrackStyleStack) { stack in + stack.append(style) + } + } + + func activeTrackStyle(@ViewBuilder content: @escaping (ActiveTrackConfiguration) -> some View) -> some View { + self.transformEnvironment(\.activeTrackStyleStack) { stack in + let style = AnyActiveTrackStyle(content) + stack.append(style) + } + } +} + // MARK: ActivityItemStyle public extension View { @@ -496,6 +513,23 @@ public extension View { } } +// MARK: FioriSliderStyle + +public extension View { + func fioriSliderStyle(_ style: some FioriSliderStyle) -> some View { + self.transformEnvironment(\.fioriSliderStyleStack) { stack in + stack.append(style) + } + } + + func fioriSliderStyle(@ViewBuilder content: @escaping (FioriSliderConfiguration) -> some View) -> some View { + self.transformEnvironment(\.fioriSliderStyleStack) { stack in + let style = AnyFioriSliderStyle(content) + stack.append(style) + } + } +} + // MARK: FootnoteStyle public extension View { @@ -683,6 +717,23 @@ public extension View { } } +// MARK: InactiveTrackStyle + +public extension View { + func inactiveTrackStyle(_ style: some InactiveTrackStyle) -> some View { + self.transformEnvironment(\.inactiveTrackStyleStack) { stack in + stack.append(style) + } + } + + func inactiveTrackStyle(@ViewBuilder content: @escaping (InactiveTrackConfiguration) -> some View) -> some View { + self.transformEnvironment(\.inactiveTrackStyleStack) { stack in + let style = AnyInactiveTrackStyle(content) + stack.append(style) + } + } +} + // MARK: IncrementActionStyle public extension View { @@ -802,6 +853,23 @@ public extension View { } } +// MARK: LeadingAccessoryStyle + +public extension View { + func leadingAccessoryStyle(_ style: some LeadingAccessoryStyle) -> some View { + self.transformEnvironment(\.leadingAccessoryStyleStack) { stack in + stack.append(style) + } + } + + func leadingAccessoryStyle(@ViewBuilder content: @escaping (LeadingAccessoryConfiguration) -> some View) -> some View { + self.transformEnvironment(\.leadingAccessoryStyleStack) { stack in + let style = AnyLeadingAccessoryStyle(content) + stack.append(style) + } + } +} + // MARK: LinearProgressIndicatorStyle public extension View { @@ -904,6 +972,23 @@ public extension View { } } +// MARK: LowerThumbStyle + +public extension View { + func lowerThumbStyle(_ style: some LowerThumbStyle) -> some View { + self.transformEnvironment(\.lowerThumbStyleStack) { stack in + stack.append(style) + } + } + + func lowerThumbStyle(@ViewBuilder content: @escaping (LowerThumbConfiguration) -> some View) -> some View { + self.transformEnvironment(\.lowerThumbStyleStack) { stack in + let style = AnyLowerThumbStyle(content) + stack.append(style) + } + } +} + // MARK: MandatoryFieldIndicatorStyle public extension View { @@ -1278,6 +1363,23 @@ public extension View { } } +// MARK: RangeSliderControlStyle + +public extension View { + func rangeSliderControlStyle(_ style: some RangeSliderControlStyle) -> some View { + self.transformEnvironment(\.rangeSliderControlStyleStack) { stack in + stack.append(style) + } + } + + func rangeSliderControlStyle(@ViewBuilder content: @escaping (RangeSliderControlConfiguration) -> some View) -> some View { + self.transformEnvironment(\.rangeSliderControlStyleStack) { stack in + let style = AnyRangeSliderControlStyle(content) + stack.append(style) + } + } +} + // MARK: RatingControlStyle public extension View { @@ -1941,6 +2043,40 @@ public extension View { } } +// MARK: TrailingAccessoryStyle + +public extension View { + func trailingAccessoryStyle(_ style: some TrailingAccessoryStyle) -> some View { + self.transformEnvironment(\.trailingAccessoryStyleStack) { stack in + stack.append(style) + } + } + + func trailingAccessoryStyle(@ViewBuilder content: @escaping (TrailingAccessoryConfiguration) -> some View) -> some View { + self.transformEnvironment(\.trailingAccessoryStyleStack) { stack in + let style = AnyTrailingAccessoryStyle(content) + stack.append(style) + } + } +} + +// MARK: UpperThumbStyle + +public extension View { + func upperThumbStyle(_ style: some UpperThumbStyle) -> some View { + self.transformEnvironment(\.upperThumbStyleStack) { stack in + stack.append(style) + } + } + + func upperThumbStyle(@ViewBuilder content: @escaping (UpperThumbConfiguration) -> some View) -> some View { + self.transformEnvironment(\.upperThumbStyleStack) { stack in + let style = AnyUpperThumbStyle(content) + stack.append(style) + } + } +} + // MARK: ValueStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index 6ffb7e45c..e87514690 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -15,6 +15,12 @@ extension Action: _ViewEmptyChecking { } } +extension ActiveTrack: _ViewEmptyChecking { + public var isEmpty: Bool { + activeTrack.isEmpty + } +} + extension ActivityItem: _ViewEmptyChecking { public var isEmpty: Bool { icon.isEmpty && @@ -230,6 +236,21 @@ extension FilledIcon: _ViewEmptyChecking { } } +extension FioriSlider: _ViewEmptyChecking { + public var isEmpty: Bool { + title.isEmpty && + valueLabel.isEmpty && + lowerThumb.isEmpty && + upperThumb.isEmpty && + activeTrack.isEmpty && + inactiveTrack.isEmpty && + icon.isEmpty && + description.isEmpty && + leadingAccessory.isEmpty && + trailingAccessory.isEmpty + } +} + extension Footnote: _ViewEmptyChecking { public var isEmpty: Bool { footnote.isEmpty @@ -300,6 +321,12 @@ extension IllustratedMessage: _ViewEmptyChecking { } } +extension InactiveTrack: _ViewEmptyChecking { + public var isEmpty: Bool { + inactiveTrack.isEmpty + } +} + extension IncrementAction: _ViewEmptyChecking { public var isEmpty: Bool { incrementAction.isEmpty @@ -350,6 +377,12 @@ extension LabelItem: _ViewEmptyChecking { } } +extension LeadingAccessory: _ViewEmptyChecking { + public var isEmpty: Bool { + leadingAccessory.isEmpty + } +} + extension LinearProgressIndicator: _ViewEmptyChecking { public var isEmpty: Bool { false @@ -397,6 +430,12 @@ extension LoadingIndicator: _ViewEmptyChecking { } } +extension LowerThumb: _ViewEmptyChecking { + public var isEmpty: Bool { + lowerThumb.isEmpty + } +} + extension MandatoryFieldIndicator: _ViewEmptyChecking { public var isEmpty: Bool { mandatoryFieldIndicator.isEmpty @@ -548,6 +587,15 @@ extension ProgressIndicatorProtocol: _ViewEmptyChecking { } } +extension RangeSliderControl: _ViewEmptyChecking { + public var isEmpty: Bool { + lowerThumb.isEmpty && + upperThumb.isEmpty && + activeTrack.isEmpty && + inactiveTrack.isEmpty + } +} + extension RatingControl: _ViewEmptyChecking { public var isEmpty: Bool { valueLabel.isEmpty && @@ -824,6 +872,18 @@ extension TopDivider: _ViewEmptyChecking { } } +extension TrailingAccessory: _ViewEmptyChecking { + public var isEmpty: Bool { + trailingAccessory.isEmpty + } +} + +extension UpperThumb: _ViewEmptyChecking { + public var isEmpty: Bool { + upperThumb.isEmpty + } +} + extension Value: _ViewEmptyChecking { public var isEmpty: Bool { value.isEmpty diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index f82bc59cd..300098bdb 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -301,3 +301,63 @@ /* Progress Indicator accessibility label: progress halted with indicator progress */ "Progress halted, %.0f" = "Progress halted, %.0f"; + +/* XACT: Slider accessibility label */ +"This is %@ slider whose maximum value is %@ and minimum value is %@" = "This is %@ slider whose maximum value is %@ and minimum value is %@"; + +/* XACT: Slider accessibility value */ +"Current value is %@" = "Current value is %@"; + +/* XACT: Range Slider lower thumb accessibility label */ +"lower range slider thumb, current value is" = "lower range slider thumb, current value is"; + +/* XACT: Range Slider lower thumb accessibility hint */ +"you can swipe up or down to change the lower value" = "you can swipe up or down to change the lower value"; + +/* XACT: Range Slider upper thumb accessibility label */ +"upper range slider thumb, current value is" = "upper range slider thumb, current value is"; + +/* XACT: Range Slider upper thumb accessibility hint */ +"you can swipe up or down to change the upper value" = "you can swipe up or down to change the upper value"; + +/* XACT: Single editable Slider thumb accessibility label */ +"slider thumb, current value is" = "slider thumb, current value is"; + +/* XACT: Single editable Slider thumb accessibility hint */ +"you can swipe up or down to change the value" = "you can swipe up or down to change the value"; + +/* XACT: Single Slider accessibility label */ +"This is a slider, the minimum value is %@ and the maximum value is %@" = "This is a slider, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Range Slider accessibility label */ +"This is a range slider, the minimum value is %@ and the maximum value is %@" = "This is a range slider, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Single editable Slider accessibility label */ +"This is a slider that is editable, the minimum value is %@ and the maximum value is %@" = "This is a slider that is editable, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Single non-editable Slider accessibility label */ +"This is a slider that can't be edited, the minimum value is %@ and the maximum value is %@" = "This is a slider that can't be edited, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Range editable Slider accessibility label */ +"This is a range slider that is editable, the minimum value is %@ and the maximum value is %@" = "This is a range slider that is editable, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Range non-editable Slider accessibility label */ +"This is a range slider that can't be edited, the minimum value is %@ and the maximum value is %@" = "This is a range slider that can't be edited, the minimum value is %@ and the maximum value is %@"; + +/* XACT: Single editable Slider Text Field accessibility label */ +"slider value text field" = "slider value text field"; + +/* XACT: Range editable Slider Lower Value Text Field accessibility label */ +"lower value text field" = "lower value text field"; + +/* XACT: Range editable Slider Upper Value Text Field accessibility label */ +"upper value text field" = "upper value text field"; + +/* XACT: Editable Slider Text Field accessibility hint */ +"You can double tap to input a specific value" = "You can double tap to input a specific value"; + +/* XACT: Editable Slider Text Field accessibility value */ +"current value is %@" = "current value is %@"; + +/* XACT: Range Editable Slider accessibility value */ +"current lower value is %@, upper value is %@" = "current lower value is %@, upper value is %@";