From 39c1c4be0c99c41322fd01984a51368ad220de9d Mon Sep 17 00:00:00 2001 From: Xiaoyu Liu Date: Fri, 23 Aug 2024 03:39:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[JIRA:=20HCPSDKFIORIUIKI?= =?UTF-8?q?T-2708]=20avatars=20enhancement=20(#773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 [JIRA: HCPSDKFIORIUIKIT-2708] avatars enhancement * feat: 🎸 [JIRA: HCPSDKFIORIUIKIT-2708] update avatars layout * fix: 🐛 default lines limit for footnote icons text --- .../ObjectItem/ObjectItemAvatarsExample.swift | 70 +++++++- .../Models/ModelDefinitions.swift | 2 + .../Views/CustomBuilder/AvatarsBuilder.swift | 40 +++-- .../CustomBuilder/FootnoteIconsBuilder.swift | 72 +++++--- .../CustomBuilder/FootnoteIconsListView.swift | 126 +++++++++++++ .../BaseComponentProtocols.swift | 6 + .../CompositeComponentProtocols.swift | 2 +- .../FootnoteIconsTextStyle.fiori.swift | 20 +++ .../_FioriStyles/ObjectItemStyle.fiori.swift | 168 +++++++++++++++++- .../FootnoteIconsText.generated.swift | 63 +++++++ .../FootnoteIconsTextStyle.generated.swift | 28 +++ .../ObjectItem/ObjectItem.generated.swift | 11 +- .../ObjectItemStyle.generated.swift | 3 + ...entStyleProtocol+Extension.generated.swift | 35 ++++ .../EnvironmentVariables.generated.swift | 21 +++ .../ModifiedStyle.generated.swift | 28 +++ .../ResolvedStyle.generated.swift | 16 ++ .../View+Extension_.generated.swift | 17 ++ ...iewEmptyChecking+Extension.generated.swift | 7 + 19 files changed, 680 insertions(+), 55 deletions(-) create mode 100644 Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/FootnoteIconsTextStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsText.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsTextStyle.generated.swift diff --git a/Apps/Examples/Examples/FioriSwiftUICore/ObjectItem/ObjectItemAvatarsExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/ObjectItem/ObjectItemAvatarsExample.swift index b90ddf72c..3bd9ea55a 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/ObjectItem/ObjectItemAvatarsExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/ObjectItem/ObjectItemAvatarsExample.swift @@ -11,7 +11,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol { } func numberOfRowsInSection(_ section: Int) -> Int { - 4 + self.isNewObjectItem ? 8 : 4 } func titleForHeaderInSection(_ section: Int) -> String { @@ -168,7 +168,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol { Image(systemName: "person") .resizable() Text("XY") - .frame(width: 40, height: 40) + .frame(width: 30, height: 30) .background(Color.red) .foregroundColor(Color.white) }, footnoteIcons: { @@ -191,6 +191,72 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol { .footnoteIconsSize(CGSize(width: 20, height: 20)) .isFootnoteIconsCircular(false) return AnyView(oi) + case (0, 4): + let oi = ObjectItem { + Text("Title: This is a case for for long text with icons") + } subtitle: { + Text("Subtitle: this is a subtitle") + } footnoteIcons: { + Color.random + Color.random + Color.random + Color.random + Color.random + Color.random + } footnoteIconsText: { + Text("This is a very very very very very very very very very very very very very very very very very long text layout with footnote icons") + } + return AnyView(oi) + case (0, 5): + let oi = ObjectItem { + Text("This is a case for for short text with icons") + } subtitle: { + Text("Subtitle: this is a subtitle") + } footnoteIcons: { + Color.random + Color.random + Color.random + Color.random + Color.random + Color.random + } footnoteIconsText: { + Text("This is a short one.") + } + return AnyView(oi) + case (0, 6): + let oi = ObjectItem { + Text("This is a case for for long leading text with icons") + } subtitle: { + Text("Subtitle: this is a subtitle") + } footnoteIcons: { + Color.random + Color.random + Color.random + Color.random + Color.random + Color.random + } footnoteIconsText: { + Text("This is a very very very very very very very very very very very very very very very very very long text layout with footnote icons") + }.footnoteIconsTextPosition(.leading) + return AnyView(oi) + case (0, 7): + let oi = ObjectItem { + Text("This is a case for for short leading text with icons") + } subtitle: { + Text("Subtitle: this is a subtitle") + } footnoteIcons: { + Color.random + Color.random + Color.random + Color.random + Color.random + Color.random + } footnoteIconsText: { + Text("This is text with custom style.") + .font(.fiori(forTextStyle: .headline)) + .foregroundStyle(Color.random) + }.footnoteIconsTextPosition(.leading) + return AnyView(oi) default: return AnyView(_ObjectItem(title: "Lorem ipseum dolor")) } diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index e9e921007..e4584351a 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -19,6 +19,8 @@ public protocol AvatarStackModel: AvatarsComponent {} // sourcery: add_env_props = "footnoteIconsSpacing" // sourcery: add_env_props = "isFootnoteIconsCircular" // sourcery: add_env_props = "footnoteIconsMaxCount" +// sourcery: add_env_props = "footnoteIconsTextPosition" +// sourcery: add_env_props = "footnoteIconsText" public protocol FootnoteIconStackModel: FootnoteIconsComponent {} // sourcery: add_env_props = "horizontalSizeClass" diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift index a5289b286..b70241a86 100644 --- a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift @@ -14,29 +14,37 @@ public protocol AvatarList: View, _ViewEmptyChecking { public extension AvatarList { /// :nodoc: @ViewBuilder func buildAvatar(_ avatar: V) -> some View { - Group { - if isCircular { - avatar - .frame(width: size.width, height: size.height) - .clipShape(Capsule()) - .overlay { - Capsule() - .inset(by: borderWidth / 2.0) - .stroke(borderColor, lineWidth: borderWidth) - } - } else { - avatar - .frame(width: size.width, height: size.height) - .border(borderColor, width: borderWidth) - } + if isCircular { + avatar + .frame(width: size.width, height: size.height) + .clipShape(Capsule()) + .overlay { + Capsule() + .inset(by: borderWidth / 2.0) + .stroke(borderColor, lineWidth: borderWidth) + } + } else { + avatar + .frame(width: size.width, height: size.height) + .border(borderColor, width: borderWidth) } } + // This condition check if for handle recursive builder issue. + private func checkIsNestingAvatars() -> Bool { + let typeString = String(describing: V.self) + return typeString.contains("SingleAvatar= 2 { ZStack(alignment: .topLeading) { self.buildAvatar(view(at: 0)) diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift index de7ab8e6d..a42f6b483 100644 --- a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift @@ -15,30 +15,7 @@ public protocol FootnoteIconList: View, _ViewEmptyChecking { public extension FootnoteIconList { /// :nodoc: var body: some View { - HStack(spacing: spacing) { - let itemsCount = maxCount <= 0 ? count : min(count, maxCount) - ForEach(0 ..< itemsCount, id: \.self) { index in - view(at: index) - .frame(width: size.width, height: size.height) - .ifApply(isCircular) { - $0.clipShape(Capsule()) - } - .overlay { - Group { - if isCircular { - Capsule() - .inset(by: 0.33 / 2.0) - .stroke(Color.preferredColor(.separator), lineWidth: 0.33) - } else { - Rectangle() - .inset(by: 0.33 / 2.0) - .stroke(Color.preferredColor(.separator), lineWidth: 0.33) - } - } - } - } - } - .clipped() + FootnoteIconsListView(icons: self) } } @@ -150,6 +127,7 @@ public struct PairFootnoteIcon: FootnoteI @Environment(\.isFootnoteIconsCircular) var isFootnoteIconsCircular @Environment(\.footnoteIconsSpacing) var footnoteIconsSpacing @Environment(\.footnoteIconsSize) var footnoteIconsSize + public var maxCount: Int { self.footnoteIconsMaxCount } @@ -333,7 +311,26 @@ public extension EnvironmentValues { } } +struct FootnoteIconsTextPosition: EnvironmentKey { + static let defaultValue: TextPosition = .trailing +} + +public extension EnvironmentValues { + /// Text position for footnote icons. + var footnoteIconsTextPosition: TextPosition { + get { self[FootnoteIconsTextPosition.self] } + set { self[FootnoteIconsTextPosition.self] = newValue } + } +} + public extension View { + /// Specific the position of the text that drawn for footnote icons. Default value is `.trailing`. + /// - Parameter position: Text position. + /// - Returns: A view that footnote icons text with specific position. + func footnoteIconsTextPosition(_ position: TextPosition) -> some View { + environment(\.footnoteIconsTextPosition, position) + } + /// Maximum number of the footnote icons. Default value is 0. When the count is less or equal to 0, means the number is unlimited. /// ```swift /// _ObjectItem(title: "Object Item", @@ -359,7 +356,7 @@ public extension View { /// .isFootnoteIconsCircular(false) /// ``` /// - Parameter isCircular: Boolean denoting whether the footnote icons are circular. - /// - Returns: A view that footnote icons are cirlcular or not. + /// - Returns: A view that footnote icons are circular or not. func isFootnoteIconsCircular(_ isCircular: Bool) -> some View { environment(\.isFootnoteIconsCircular, isCircular) } @@ -394,3 +391,28 @@ public extension View { environment(\.footnoteIconsSize, size) } } + +/// Text position for icons. +public enum TextPosition { + /// Top position for text. + case top + /// Bottom position for text. + case bottom + /// Leading position for text. + case leading + /// Trailing position for text. + case trailing + + var alignment: Alignment { + switch self { + case .top: + return .top + case .bottom: + return .bottom + case .leading: + return .leading + case .trailing: + return .trailing + } + } +} diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift new file mode 100644 index 000000000..9b771b4e2 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift @@ -0,0 +1,126 @@ +import SwiftUI + +struct FootnoteIconsListView: View { + let icons: T + + var count: Int { + self.icons.count + } + + var maxCount: Int { + self.icons.maxCount + } + + var size: CGSize { + self.icons.size + } + + var isCircular: Bool { + self.icons.isCircular + } + + var spacing: CGFloat { + self.icons.spacing + } + + // This condition check if for handle recursive builder issue. + func checkIsNestingIcons() -> Bool { + let typeString = String(describing: T.self) + return typeString.contains("SingleFootnoteIcon some View { + FootnoteIconsHStack(spacing: self.spacing) { + let itemsCount = self.maxCount <= 0 ? self.count : min(self.count, self.maxCount) + ForEach(0 ..< itemsCount, id: \.self) { index in + self.icons.view(at: index) + .frame(width: self.size.width, height: self.size.height) + .ifApply(self.isCircular) { + $0.clipShape(Capsule()) + } + .overlay { + Group { + if self.isCircular { + Capsule() + .inset(by: 0.33 / 2.0) + .stroke(Color.preferredColor(.separator), lineWidth: 0.33) + } else { + Rectangle() + .inset(by: 0.33 / 2.0) + .stroke(Color.preferredColor(.separator), lineWidth: 0.33) + } + } + } + } + } + } +} + +struct FootnoteIconsHStack: Layout { + struct CacheData { + var width: CGFloat + var count: Int + var size: CGSize + } + + let spacing: CGFloat + + func makeCache(subviews: Subviews) -> CacheData { + CacheData(width: 0, count: 0, size: .zero) + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize { + self.calculateSizeAndCount(proposal: proposal, subviews: subviews, cache: &cache) + return cache.size + } + + func calculateSizeAndCount(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) { + guard let contentWidth = proposal.width, cache.width != contentWidth else { + return + } + cache.width = contentWidth + + var totalWidth: CGFloat = 0 + var maxHeight: CGFloat = 0 + + for (index, subview) in subviews.enumerated() { + let subviewSize = subview.sizeThatFits(proposal) + maxHeight = max(maxHeight, subviewSize.height) + if subviewSize.width + totalWidth <= contentWidth { + totalWidth += subviewSize.width + totalWidth += self.spacing + } else { + cache.count = index + cache.size = CGSize(width: totalWidth, height: maxHeight) + break + } + } + totalWidth -= self.spacing + cache.count = subviews.count + cache.size = CGSize(width: totalWidth, height: maxHeight) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) { + var xOffset: CGFloat = bounds.minX + self.calculateSizeAndCount(proposal: proposal, subviews: subviews, cache: &cache) + for (index, subview) in subviews.enumerated() { + if index < cache.count { + let subviewSize = subview.sizeThatFits(proposal) + subview.place(at: CGPoint(x: xOffset, y: bounds.minY), + proposal: ProposedViewSize(CGSize(width: subviewSize.width, height: subviewSize.height))) + xOffset += (subviewSize.width + self.spacing) + } else { + break + } + } + } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift index 6a7610ff3..bb422e118 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift @@ -89,6 +89,12 @@ protocol _FootnoteIconsComponent { var footnoteIcons: [TextOrIcon] { get } } +// sourcery: BaseComponent +protocol _FootnoteIconsTextComponent { + // sourcery: @ViewBuilder + var footnoteIconsText: AttributedString? { get } +} + // sourcery: BaseComponent protocol _AvatarsComponent { // sourcery: resultBuilder.name = @AvatarsBuilder, resultBuilder.backingComponent = AvatarStack diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 5e43a7d97..5e9e9df23 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -3,7 +3,7 @@ import SwiftUI /// A view that displays information of an object. // sourcery: CompositeComponent -protocol _ObjectItemComponent: _TitleComponent, _SubtitleComponent, _FootnoteComponent, _DescriptionComponent, _StatusComponent, _SubstatusComponent, _DetailImageComponent, _IconsComponent, _AvatarsComponent, _FootnoteIconsComponent, _TagsComponent, _ActionComponent {} +protocol _ObjectItemComponent: _TitleComponent, _SubtitleComponent, _FootnoteComponent, _DescriptionComponent, _StatusComponent, _SubstatusComponent, _DetailImageComponent, _IconsComponent, _AvatarsComponent, _FootnoteIconsComponent, _FootnoteIconsTextComponent, _TagsComponent, _ActionComponent {} // sourcery: CompositeComponent, InternalComponent protocol _DemoViewComponent: _TitleComponent, _SubtitleComponent, _StatusComponent, _ActionComponent, _SwitchComponent {} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/FootnoteIconsTextStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/FootnoteIconsTextStyle.fiori.swift new file mode 100644 index 000000000..6ffd2a405 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/FootnoteIconsTextStyle.fiori.swift @@ -0,0 +1,20 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +// Base Layout style +public struct FootnoteIconsTextBaseStyle: FootnoteIconsTextStyle { + @ViewBuilder + public func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> some View { + // Add default layout here + configuration.footnoteIconsText + } +} + +// Default fiori styles +public struct FootnoteIconsTextFioriStyle: FootnoteIconsTextStyle { + @ViewBuilder + public func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> some View { + FootnoteIconsText(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift index d857e2744..796cff2b3 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift @@ -2,6 +2,8 @@ import FioriThemeManager import Foundation import SwiftUI +// swiftlint:disable file_length + /** This file provides default fiori style for the component. @@ -16,7 +18,8 @@ public struct ObjectItemBaseStyle: ObjectItemStyle { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.splitPercent) var splitPercent @Environment(\.dynamicTypeSize) var dynamicTypeSize - + @Environment(\.footnoteIconsTextPosition) var footnoteIconsTextPosition + @State var mainViewSize: CGSize = .zero public func makeBody(_ configuration: ObjectItemConfiguration) -> some View { @@ -157,7 +160,7 @@ extension ObjectItemBaseStyle { context.configuration.subtitle context.configuration.footnote context.configuration.tags - context.configuration.footnoteIcons + self.footnoteIconsView(context) } Spacer(minLength: 16) @@ -188,7 +191,7 @@ extension ObjectItemBaseStyle { context.configuration.subtitle context.configuration.footnote context.configuration.tags - context.configuration.footnoteIcons + self.footnoteIconsView(context) } Spacer(minLength: 16) @@ -274,7 +277,7 @@ extension ObjectItemBaseStyle { context.configuration.subtitle context.configuration.footnote context.configuration.tags - context.configuration.footnoteIcons + self.footnoteIconsView(context) } Spacer(minLength: 0) } @@ -302,7 +305,7 @@ extension ObjectItemBaseStyle { context.configuration.subtitle context.configuration.footnote context.configuration.tags - context.configuration.footnoteIcons + self.footnoteIconsView(context) } Spacer(minLength: 8) @@ -383,7 +386,7 @@ extension ObjectItemBaseStyle { context.configuration.subtitle context.configuration.footnote context.configuration.tags - context.configuration.footnoteIcons + self.footnoteIconsView(context) } Spacer(minLength: 16) } @@ -491,6 +494,14 @@ extension ObjectItemBaseStyle { return 3 } } + + @ViewBuilder + func footnoteIconsView(_ context: Context) -> some View { + FootnoteIconsAndTextLayout(textPosition: self.footnoteIconsTextPosition) { + context.configuration.footnoteIconsText + context.configuration.footnoteIcons + } + } } // Default fiori styles @@ -600,6 +611,17 @@ extension ObjectItemFioriStyle { // Add default style here } } + + struct FootnoteIconsTextFioriStyle: FootnoteIconsTextStyle { + let objectItemConfiguration: ObjectItemConfiguration + + func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> some View { + FootnoteIconsText(configuration) + .font(.fiori(forTextStyle: .subheadline)) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + .lineLimit(1) + } + } struct TagsFioriStyle: TagsStyle { let objectItemConfiguration: ObjectItemConfiguration @@ -651,7 +673,7 @@ public struct ObjectItemBorderedAction: ActionStyle { } } -#Preview(body: { +#Preview { List { ObjectItem(title: { Text("Title") @@ -671,10 +693,140 @@ public struct ObjectItemBorderedAction: ActionStyle { Text("1") Circle().fill(Color.preferredColor(.tintColor)).frame(width: 14, height: 14) Image(systemName: "paperclip").font(.system(size: 14)) + }, footnoteIcons: { + Color.red + Color.green + Color.blue + Color.red + Color.green + Color.blue + Color.red + Color.green + Color.blue + }, footnoteIconsText: { + Text("Footnote icons text.") }) .titleStyle { config in config.title .foregroundStyle(.blue) // take effect } } -}) +} + +struct FootnoteIconsAndTextLayout: Layout { + let textPosition: TextPosition + let margin = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0) + let textAndIconsSpacing: CGFloat = 6 + let textMinimumWidth: CGFloat = 60 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { + guard let containerWidth = proposal.width, containerWidth > 0 else { + return .zero + } + if subviews.count == 2, + let textView = subviews.first, + let iconsView = subviews.last, + textView.sizeThatFits(.infinity) != .zero, + iconsView.sizeThatFits(.infinity) != .zero + { + switch self.textPosition { + case .top, .bottom: + let availableWidth = containerWidth - self.margin.leading - self.margin.trailing + let textViewSize = textView.sizeThatFits(ProposedViewSize(width: availableWidth, + height: .infinity)) + let iconsSize = iconsView.sizeThatFits(ProposedViewSize(width: availableWidth, + height: .infinity)) + let maxHeight = textViewSize.height + iconsSize.height + self.textAndIconsSpacing + self.margin.top + self.margin.bottom + return CGSize(width: containerWidth, + height: maxHeight) + case .leading, .trailing: + let textActualSize = textView.sizeThatFits(.unspecified) + let iconsAvailableWidth = containerWidth - min(textActualSize.width, self.textMinimumWidth) - self.margin.leading - self.margin.trailing + let iconsSize = iconsView.sizeThatFits(ProposedViewSize(width: iconsAvailableWidth, + height: .infinity)) + let textWidth = containerWidth - iconsSize.width - self.margin.leading - self.margin.trailing - self.textAndIconsSpacing + let textSize = textView.sizeThatFits(ProposedViewSize(width: textWidth, height: .infinity)) + let maxHeight = max(iconsSize.height, textSize.height) + self.margin.top + self.margin.bottom + let size = CGSize(width: containerWidth, height: maxHeight) + return size + } + } else { + var maxHeight: CGFloat = 0 + let contentWidth = containerWidth - self.margin.leading - self.margin.trailing + for subview in subviews { + let height = subview.sizeThatFits(ProposedViewSize(width: contentWidth, height: .infinity)).height + maxHeight = max(maxHeight, height) + } + return CGSize(width: containerWidth, height: maxHeight + self.margin.top + self.margin.bottom) + } + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { + guard let containerWidth = proposal.width, containerWidth > 0 else { + return + } + if subviews.count == 2, + let textView = subviews.first, + let iconsView = subviews.last, + textView.sizeThatFits(.infinity) != .zero, + iconsView.sizeThatFits(.infinity) != .zero + { + switch self.textPosition { + case .top: + textView.place(at: bounds.origin, proposal: .unspecified) + let nextOrigin = CGPoint(x: bounds.minX, + y: bounds.minY + textView.sizeThatFits(.unspecified).height + self.textAndIconsSpacing) + iconsView.place(at: nextOrigin, proposal: .unspecified) + case .bottom: + iconsView.place(at: bounds.origin, proposal: .unspecified) + let nextOrigin = CGPoint(x: bounds.minX, + y: bounds.minY + textView.sizeThatFits(.unspecified).height + self.textAndIconsSpacing) + textView.place(at: nextOrigin, proposal: .unspecified) + case .leading: + let textActualSize = textView.sizeThatFits(.unspecified) + let iconsAvailableWidth = containerWidth - min(textActualSize.width, self.textMinimumWidth) - self.textAndIconsSpacing - self.margin.leading - self.margin.trailing + let iconsSize = iconsView.sizeThatFits(ProposedViewSize(width: iconsAvailableWidth, height: .infinity)) + let textMaxWidth = containerWidth - iconsSize.width - self.textAndIconsSpacing - self.margin.leading - self.margin.trailing + let textSize: CGSize + if textActualSize.width < textMaxWidth { + textSize = textActualSize + } else { + textSize = textView.sizeThatFits(ProposedViewSize(CGSize(width: textMaxWidth, height: .infinity))) + } + let maxHeight = max(textSize.height, iconsSize.height) + let iconsY = bounds.minY + (maxHeight - iconsSize.height) / 2 + self.margin.top + let textY = bounds.minY + (maxHeight - textSize.height) / 2 + self.margin.top + + textView.place(at: CGPoint(x: bounds.minX + self.margin.leading, y: textY), proposal: ProposedViewSize(textSize)) + let iconsX = bounds.origin.x + textSize.width + self.textAndIconsSpacing + self.margin.leading + iconsView.place(at: CGPoint(x: iconsX, y: iconsY), + proposal: ProposedViewSize(iconsSize)) + case .trailing: + let textActualSize = textView.sizeThatFits(.unspecified) + let iconsAvailableWidth = containerWidth - min(textActualSize.width, self.textMinimumWidth) - self.textAndIconsSpacing - self.margin.leading - self.margin.trailing + let iconsSize = iconsView.sizeThatFits(ProposedViewSize(width: iconsAvailableWidth, height: .infinity)) + let textMaxWidth = containerWidth - iconsSize.width - self.textAndIconsSpacing - self.margin.leading - self.margin.trailing + + let textX = bounds.minX + iconsSize.width + self.textAndIconsSpacing + self.margin.leading + let textSize: CGSize + if textActualSize.width < textMaxWidth { + textSize = textActualSize + } else { + textSize = textView.sizeThatFits(ProposedViewSize(CGSize(width: textMaxWidth, height: .infinity))) + } + let maxHeight = max(textSize.height, iconsSize.height) + let iconsY = bounds.minY + (maxHeight - iconsSize.height) / 2 + self.margin.top + let textY = bounds.minY + (maxHeight - textSize.height) / 2 + self.margin.top + iconsView.place(at: CGPoint(x: bounds.minX + self.margin.leading, y: iconsY), + proposal: ProposedViewSize(iconsSize)) + textView.place(at: CGPoint(x: textX, y: textY), proposal: ProposedViewSize(textSize)) + } + } else { + for subview in subviews { + subview.place(at: CGPoint(x: bounds.origin.x + self.margin.leading, + y: bounds.origin.y + self.margin.top), + proposal: ProposedViewSize(CGSize(width: containerWidth - self.margin.leading - self.margin.trailing, height: .infinity))) + } + } + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsText.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsText.generated.swift new file mode 100644 index 000000000..4fc6b540a --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsText.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 FootnoteIconsText { + let footnoteIconsText: any View + + @Environment(\.footnoteIconsTextStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder footnoteIconsText: () -> any View = { EmptyView() }) { + self.footnoteIconsText = footnoteIconsText() + } +} + +public extension FootnoteIconsText { + init(footnoteIconsText: AttributedString? = nil) { + self.init(footnoteIconsText: { OptionalText(footnoteIconsText) }) + } +} + +public extension FootnoteIconsText { + init(_ configuration: FootnoteIconsTextConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: FootnoteIconsTextConfiguration, shouldApplyDefaultStyle: Bool) { + self.footnoteIconsText = configuration.footnoteIconsText + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension FootnoteIconsText: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(footnoteIconsText: .init(self.footnoteIconsText))).typeErased + .transformEnvironment(\.footnoteIconsTextStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension FootnoteIconsText { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + FootnoteIconsText(footnoteIconsText: { self.footnoteIconsText }) + .shouldApplyDefaultStyle(false) + .footnoteIconsTextStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsTextStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsTextStyle.generated.swift new file mode 100644 index 000000000..6079ae4ae --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/FootnoteIconsText/FootnoteIconsTextStyle.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 FootnoteIconsTextStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> Body +} + +struct AnyFootnoteIconsTextStyle: FootnoteIconsTextStyle { + let content: (FootnoteIconsTextConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (FootnoteIconsTextConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct FootnoteIconsTextConfiguration { + public let footnoteIconsText: FootnoteIconsText + + public typealias FootnoteIconsText = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift index 5b3e590b7..1b319d574 100755 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift @@ -15,6 +15,7 @@ public struct ObjectItem { let icons: any View let avatars: any View let footnoteIcons: any View + let footnoteIconsText: any View let tags: any View let action: any View @@ -32,6 +33,7 @@ public struct ObjectItem { @IconBuilder icons: () -> any View = { EmptyView() }, @AvatarsBuilder avatars: () -> any View = { EmptyView() }, @FootnoteIconsBuilder footnoteIcons: () -> any View = { EmptyView() }, + @ViewBuilder footnoteIconsText: () -> any View = { EmptyView() }, @TagBuilder tags: () -> any View = { EmptyView() }, @ViewBuilder action: () -> any View = { EmptyView() }) { @@ -45,6 +47,7 @@ public struct ObjectItem { self.icons = Icons { icons() } self.avatars = Avatars { avatars() } self.footnoteIcons = FootnoteIcons { footnoteIcons() } + self.footnoteIconsText = FootnoteIconsText { footnoteIconsText() } self.tags = Tags { tags() } self.action = Action { action() } } @@ -61,10 +64,11 @@ public extension ObjectItem { icons: [TextOrIcon] = [], avatars: [TextOrIcon] = [], footnoteIcons: [TextOrIcon] = [], + footnoteIconsText: AttributedString? = nil, tags: [AttributedString] = [], action: FioriButton? = nil) { - self.init(title: { Text(title) }, subtitle: { OptionalText(subtitle) }, footnote: { OptionalText(footnote) }, description: { OptionalText(description) }, status: { TextOrIconView(status) }, substatus: { TextOrIconView(substatus) }, detailImage: { detailImage }, icons: { IconStack(icons) }, avatars: { AvatarStack(avatars) }, footnoteIcons: { FootnoteIconStack(footnoteIcons) }, tags: { TagStack(tags) }, action: { action }) + self.init(title: { Text(title) }, subtitle: { OptionalText(subtitle) }, footnote: { OptionalText(footnote) }, description: { OptionalText(description) }, status: { TextOrIconView(status) }, substatus: { TextOrIconView(substatus) }, detailImage: { detailImage }, icons: { IconStack(icons) }, avatars: { AvatarStack(avatars) }, footnoteIcons: { FootnoteIconStack(footnoteIcons) }, footnoteIconsText: { OptionalText(footnoteIconsText) }, tags: { TagStack(tags) }, action: { action }) } } @@ -84,6 +88,7 @@ public extension ObjectItem { self.icons = configuration.icons self.avatars = configuration.avatars self.footnoteIcons = configuration.footnoteIcons + self.footnoteIconsText = configuration.footnoteIconsText self.tags = configuration.tags self.action = configuration.action self._shouldApplyDefaultStyle = shouldApplyDefaultStyle @@ -95,7 +100,7 @@ extension ObjectItem: View { if self._shouldApplyDefaultStyle { self.defaultStyle() } else { - self.style.resolve(configuration: .init(title: .init(self.title), subtitle: .init(self.subtitle), footnote: .init(self.footnote), description: .init(self.description), status: .init(self.status), substatus: .init(self.substatus), detailImage: .init(self.detailImage), icons: .init(self.icons), avatars: .init(self.avatars), footnoteIcons: .init(self.footnoteIcons), tags: .init(self.tags), action: .init(self.action))).typeErased + self.style.resolve(configuration: .init(title: .init(self.title), subtitle: .init(self.subtitle), footnote: .init(self.footnote), description: .init(self.description), status: .init(self.status), substatus: .init(self.substatus), detailImage: .init(self.detailImage), icons: .init(self.icons), avatars: .init(self.avatars), footnoteIcons: .init(self.footnoteIcons), footnoteIconsText: .init(self.footnoteIconsText), tags: .init(self.tags), action: .init(self.action))).typeErased .transformEnvironment(\.objectItemStyleStack) { stack in if !stack.isEmpty { stack.removeLast() @@ -113,7 +118,7 @@ private extension ObjectItem { } func defaultStyle() -> some View { - ObjectItem(.init(title: .init(self.title), subtitle: .init(self.subtitle), footnote: .init(self.footnote), description: .init(self.description), status: .init(self.status), substatus: .init(self.substatus), detailImage: .init(self.detailImage), icons: .init(self.icons), avatars: .init(self.avatars), footnoteIcons: .init(self.footnoteIcons), tags: .init(self.tags), action: .init(self.action))) + ObjectItem(.init(title: .init(self.title), subtitle: .init(self.subtitle), footnote: .init(self.footnote), description: .init(self.description), status: .init(self.status), substatus: .init(self.substatus), detailImage: .init(self.detailImage), icons: .init(self.icons), avatars: .init(self.avatars), footnoteIcons: .init(self.footnoteIcons), footnoteIconsText: .init(self.footnoteIconsText), tags: .init(self.tags), action: .init(self.action))) .shouldApplyDefaultStyle(false) .objectItemStyle(ObjectItemFioriStyle.ContentFioriStyle()) .typeErased diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItemStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItemStyle.generated.swift index e23e25064..03804f1ca 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItemStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItemStyle.generated.swift @@ -32,6 +32,7 @@ public struct ObjectItemConfiguration { public let icons: Icons public let avatars: Avatars public let footnoteIcons: FootnoteIcons + public let footnoteIconsText: FootnoteIconsText public let tags: Tags public let action: Action @@ -45,6 +46,7 @@ public struct ObjectItemConfiguration { public typealias Icons = ConfigurationViewWrapper public typealias Avatars = ConfigurationViewWrapper public typealias FootnoteIcons = ConfigurationViewWrapper + public typealias FootnoteIconsText = ConfigurationViewWrapper public typealias Tags = ConfigurationViewWrapper public typealias Action = ConfigurationViewWrapper } @@ -62,6 +64,7 @@ public struct ObjectItemFioriStyle: ObjectItemStyle { .iconsStyle(IconsFioriStyle(objectItemConfiguration: configuration)) .avatarsStyle(AvatarsFioriStyle(objectItemConfiguration: configuration)) .footnoteIconsStyle(FootnoteIconsFioriStyle(objectItemConfiguration: configuration)) + .footnoteIconsTextStyle(FootnoteIconsTextFioriStyle(objectItemConfiguration: configuration)) .tagsStyle(TagsFioriStyle(objectItemConfiguration: configuration)) .actionStyle(ActionFioriStyle(objectItemConfiguration: configuration)) } diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index 365a5c36a..628f967ee 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -1599,6 +1599,20 @@ public extension FootnoteIconsStyle where Self == FootnoteIconsFioriStyle { } } +// MARK: FootnoteIconsTextStyle + +public extension FootnoteIconsTextStyle where Self == FootnoteIconsTextBaseStyle { + static var base: FootnoteIconsTextBaseStyle { + FootnoteIconsTextBaseStyle() + } +} + +public extension FootnoteIconsTextStyle where Self == FootnoteIconsTextFioriStyle { + static var fiori: FootnoteIconsTextFioriStyle { + FootnoteIconsTextFioriStyle() + } +} + // MARK: FormViewStyle public extension FormViewStyle where Self == FormViewBaseStyle { @@ -2768,6 +2782,27 @@ public extension ObjectItemStyle where Self == ObjectItemFootnoteIconsStyle { } } +public struct ObjectItemFootnoteIconsTextStyle: ObjectItemStyle { + let style: any FootnoteIconsTextStyle + + public func makeBody(_ configuration: ObjectItemConfiguration) -> some View { + ObjectItem(configuration) + .footnoteIconsTextStyle(self.style) + .typeErased + } +} + +public extension ObjectItemStyle where Self == ObjectItemFootnoteIconsTextStyle { + static func footnoteIconsTextStyle(_ style: some FootnoteIconsTextStyle) -> ObjectItemFootnoteIconsTextStyle { + ObjectItemFootnoteIconsTextStyle(style: style) + } + + static func footnoteIconsTextStyle(@ViewBuilder content: @escaping (FootnoteIconsTextConfiguration) -> some View) -> ObjectItemFootnoteIconsTextStyle { + let style = AnyFootnoteIconsTextStyle(content) + return ObjectItemFootnoteIconsTextStyle(style: style) + } +} + public struct ObjectItemTagsStyle: ObjectItemStyle { let style: any TagsStyle diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index 9dc0501e5..93458187f 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -444,6 +444,27 @@ extension EnvironmentValues { } } +// MARK: FootnoteIconsTextStyle + +struct FootnoteIconsTextStyleStackKey: EnvironmentKey { + static let defaultValue: [any FootnoteIconsTextStyle] = [] +} + +extension EnvironmentValues { + var footnoteIconsTextStyle: any FootnoteIconsTextStyle { + self.footnoteIconsTextStyleStack.last ?? .base + } + + var footnoteIconsTextStyleStack: [any FootnoteIconsTextStyle] { + get { + self[FootnoteIconsTextStyleStackKey.self] + } + set { + self[FootnoteIconsTextStyleStackKey.self] = newValue + } + } +} + // MARK: FormViewStyle struct FormViewStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index 389528480..6e3e6cf6e 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -596,6 +596,34 @@ public extension FootnoteIconsStyle { } } +// MARK: FootnoteIconsTextStyle + +extension ModifiedStyle: FootnoteIconsTextStyle where Style: FootnoteIconsTextStyle { + public func makeBody(_ configuration: FootnoteIconsTextConfiguration) -> some View { + FootnoteIconsText(configuration) + .footnoteIconsTextStyle(self.style) + .modifier(self.modifier) + } +} + +public struct FootnoteIconsTextStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.footnoteIconsTextStyle(self.style) + } +} + +public extension FootnoteIconsTextStyle { + func modifier(_ modifier: some ViewModifier) -> some FootnoteIconsTextStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some FootnoteIconsTextStyle) -> some FootnoteIconsTextStyle { + style.modifier(FootnoteIconsTextStyleModifier(style: self)) + } +} + // MARK: FormViewStyle extension ModifiedStyle: FormViewStyle where Style: FormViewStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index d573b5308..ce2a30855 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -339,6 +339,22 @@ extension FootnoteIconsStyle { } } +// MARK: FootnoteIconsTextStyle + +struct ResolvedFootnoteIconsTextStyle: View { + let style: Style + let configuration: FootnoteIconsTextConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension FootnoteIconsTextStyle { + func resolve(configuration: FootnoteIconsTextConfiguration) -> some View { + ResolvedFootnoteIconsTextStyle(style: self, configuration: configuration) + } +} + // MARK: FormViewStyle struct ResolvedFormViewStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index 8cca73e2f..be8fd660a 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -360,6 +360,23 @@ public extension View { } } +// MARK: FootnoteIconsTextStyle + +public extension View { + func footnoteIconsTextStyle(_ style: some FootnoteIconsTextStyle) -> some View { + self.transformEnvironment(\.footnoteIconsTextStyleStack) { stack in + stack.append(style) + } + } + + func footnoteIconsTextStyle(@ViewBuilder content: @escaping (FootnoteIconsTextConfiguration) -> some View) -> some View { + self.transformEnvironment(\.footnoteIconsTextStyleStack) { stack in + let style = AnyFootnoteIconsTextStyle(content) + stack.append(style) + } + } +} + // MARK: FormViewStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index 98d73c594..b9c915603 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -177,6 +177,12 @@ extension FootnoteIcons: _ViewEmptyChecking { } } +extension FootnoteIconsText: _ViewEmptyChecking { + public var isEmpty: Bool { + footnoteIconsText.isEmpty + } +} + extension FormView: _ViewEmptyChecking { public var isEmpty: Bool { false @@ -348,6 +354,7 @@ extension ObjectItem: _ViewEmptyChecking { icons.isEmpty && avatars.isEmpty && footnoteIcons.isEmpty && + footnoteIconsText.isEmpty && tags.isEmpty && action.isEmpty }