From 83cd79543c0b8f753d0bdcf67d8653e1724cd4e7 Mon Sep 17 00:00:00 2001 From: urmitchauhan <105660080+urmitchauhan@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:09:52 +0530 Subject: [PATCH] Fix: [macOS] collections element bleeds to bottom of card instead of just the edges (#396) * Fix: [macOS] collections element bleeds to bottom of card instead of just the edges - refactor bleed configuration at base render - add new methods in collection element view - bug fix - add new unit test for bleed * bleed focus revert * make bleed value struct --- .../samples/ContainerBleedWithImage.json | 50 ++++++++ .../samples/ContainerSelectAction.json | 6 +- .../Renderers/AdaptiveCardRenderer.swift | 6 +- .../Renderers/BaseCardElementRenderer.swift | 96 ++++----------- .../Renderers/ColumnRenderer.swift | 2 +- .../Renderers/ColumnSetRenderer.swift | 3 +- .../Renderers/ContainerRenderer.swift | 2 +- .../AdaptiveCards/Utils/BleedUtils.swift | 15 +++ .../Views/ACRColumnSetView.swift | 6 + .../AdaptiveCards/Views/ACRColumnView.swift | 28 +++-- .../Views/ACRContainerView.swift | 28 +++-- .../Views/ACRContentStackView.swift | 109 ++++++++++++------ .../AdaptiveCardsTests/Fakes/FakeColumn.swift | 9 ++ .../Fakes/FakeColumnSet.swift | 20 +++- .../Fakes/FakeContainer.swift | 8 ++ .../Renderers/ColumnRendererTests.swift | 30 +++++ .../Renderers/ColumnSetRendererTests.swift | 24 ++++ .../Renderers/ContainerRendererTests.swift | 30 +++++ 18 files changed, 326 insertions(+), 146 deletions(-) create mode 100644 source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerBleedWithImage.json diff --git a/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerBleedWithImage.json b/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerBleedWithImage.json new file mode 100644 index 0000000000..b77262d184 --- /dev/null +++ b/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerBleedWithImage.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.3", + "body": [ + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "Container", + "style": "good", + "items": [ + { + "type": "TextBlock", + "text": "This Container bleeds into its parent padding for a nice visual appearance!", + "wrap": true + } + ], + "bleed": true + }, + { + "type": "Container", + "style": "warning", + "bleed": true, + "items": [ + { + "type": "TextBlock", + "text": "This Container is just a plain ol' container, it looks nice too though!", + "wrap": true + } + ] + } + ], + "bleed": true, + "backgroundImage": { + "url": "https://picsum.photos/200/200" + } + } + ], + "actions": [ + { + "type": "Action.ShowCard", + "title": "Action.ShowCard", + "card": { + "type": "AdaptiveCard" + } + } + ] +} diff --git a/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerSelectAction.json b/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerSelectAction.json index 3236ce62ee..167297d498 100644 --- a/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerSelectAction.json +++ b/source/macos/ADCMacOSVisualizer/ADCMacOSVisualizer/samples/ContainerSelectAction.json @@ -185,7 +185,8 @@ "data": { "info": "My submit action data" } - } + }, + "bleed": true }, { "type": "TextBlock", @@ -224,7 +225,8 @@ } ] } - ] + ], + "bleed": true }, { "type": "Column", diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/AdaptiveCardRenderer.swift b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/AdaptiveCardRenderer.swift index 857e555b32..cd6b5d20e5 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/AdaptiveCardRenderer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/AdaptiveCardRenderer.swift @@ -63,7 +63,7 @@ class AdaptiveCardRenderer { let renderer = RendererManager.shared.renderer(for: element.getType()) let view = renderer.render(element: element, with: hostConfig, style: style, rootView: rootView, parentView: rootView, inputs: [], config: config) BaseCardElementRenderer.shared.updateLayoutForSeparatorAndAlignment(view: view, element: element, parentView: rootView, rootView: rootView, style: style, hostConfig: hostConfig, config: config, isfirstElement: isFirstElement) - BaseCardElementRenderer.shared.configBleed(collectionView: view, parentView: rootView, with: hostConfig, element: element, parentElement: nil) + BaseCardElementRenderer.shared.configBleed(for: view, with: hostConfig, element: element) } if !card.getActions().isEmpty { @@ -85,6 +85,10 @@ class AdaptiveCardRenderer { if let backgroundImage = card.getBackgroundImage(), let url = backgroundImage.getUrl() { rootView.setupBackgroundImageProperties(backgroundImage) + let padding = CGFloat(truncating: hostConfig.getSpacing()?.paddingSpacing ?? 0) + // bleed view setup + rootView.setBleedViewConstraint(direction: ACRBleedValue(top: true, bottom: true, leading: true, trailing: true), with: padding) + rootView.bleedBackgroundImage(direction: ACRBleedValue(top: true, bottom: true, leading: true, trailing: true), with: padding) rootView.registerImageHandlingView(rootView.backgroundImageView, for: url) } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/BaseCardElementRenderer.swift b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/BaseCardElementRenderer.swift index c376941dbe..d3fef553e8 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/BaseCardElementRenderer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/BaseCardElementRenderer.swift @@ -58,86 +58,30 @@ class BaseCardElementRenderer { view.setContentHuggingPriority(kFitViewHorizontalLayoutConstraintPriority, for: .horizontal) } - func configBleed(collectionView: NSView, parentView: ACRContentStackView, with hostConfig: ACSHostConfig, element: ACSBaseCardElement, parentElement: ACSBaseCardElement?) { - guard let collection = element as? ACSStyledCollectionElement, collection.getBleed() else { - return - } - guard let collectionView = collectionView as? ACRContentStackView else { + func configBleed(for view: NSView, with hostConfig: ACSHostConfig, element: ACSBaseCardElement) { + guard let collectionElem = element as? ACSStyledCollectionElement, collectionElem.getBleed() else { return } + guard let collectionView = view as? ACRContentStackView else { logError("Container is not type of ACRContentStackView") return } - // bleed specification requires the object that's asked to be bled to have padding - if collection.getPadding() { - let adaptiveBleedDirection = collection.getBleedDirection() - let direction = ACRBleedDirection(rawValue: adaptiveBleedDirection.rawValue) - - // 1. create a background view (bv). - // 2. Now, add bv to the parentView - // bv is then pinned to the parentView to the bleed direction - // bv gets current collection view's (cv) container style - // container view's stack view (csv) holds content views, and bv dislpays - // container style we transpose them, and get the final result - - if !collection.getCanBleed() { - return - } - - let top = ((direction.rawValue & ACRBleedDirection.ACRBleedToTopEdge.rawValue) != 0) - let leading = ((direction.rawValue & ACRBleedDirection.ACRBleedToLeadingEdge.rawValue) != 0) - let trailing = ((direction.rawValue & ACRBleedDirection.ACRBleedToTrailingEdge.rawValue) != 0) - let bottom = ((direction.rawValue & ACRBleedDirection.ACRBleedToBottomEdge.rawValue) != 0) - - var padding: CGFloat = 0 - if let paddingSpace = hostConfig.getSpacing()?.paddingSpacing { - padding = CGFloat(truncating: paddingSpace) - } - collectionView.setBleedProp(top: top, bottom: bottom, trailing: trailing, leading: leading) - var paddingBottom: CGFloat = 0 - var bottomAnchor: NSLayoutAnchor = collectionView.bottomAnchor - if bottom { - // case 1: when parent style == none, - // case 2: when parent is bleeding but, bottom bleed direction false - // case 3: when parent bleeds till end and has bottom bleed direction true - var parentBottomBleedDirection = false - if let parent = parentElement as? ACSStyledCollectionElement { - let directionParent = parent.getBleedDirection() - parentBottomBleedDirection = (directionParent.rawValue & ACRBleedDirection.ACRBleedToBottomEdge.rawValue) != 0 - } - if let parentCollection = parentElement as? ACSStyledCollectionElement { - paddingBottom = !parentCollection.getPadding() || (parentView.bleed && parentBottomBleedDirection - ) ? padding : 0 - } - bottomAnchor = parentView.bottomAnchor - } - - if collection.getBackgroundImage() != nil { - if let collectionView = collectionView as? ACRColumnView { - collectionView.bleedBackgroundImage(padding: padding, top: top, bottom: bottom, leading: leading, trailing: trailing, paddingBottom: paddingBottom, with: bottomAnchor) - } - if let collectionView = collectionView as? ACRContainerView { - collectionView.bleedBackgroundImage(padding: padding, top: top, bottom: bottom, leading: leading, trailing: trailing, paddingBottom: paddingBottom, with: bottomAnchor) - } - return - } - let backgroundView = NSView() - backgroundView.translatesAutoresizingMaskIntoConstraints = false - - // adding this above collectionView backgroundImage view - collectionView.addSubview(backgroundView, positioned: .below, relativeTo: collectionView.stackView) - backgroundView.wantsLayer = true - backgroundView.layer?.backgroundColor = collectionView.layer?.backgroundColor - - backgroundView.topAnchor.constraint(equalTo: collectionView.topAnchor, constant: top ? -padding : 0).isActive = true - backgroundView.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor, constant: leading ? -padding : 0).isActive = true - backgroundView.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor, constant: trailing ? padding : 0).isActive = true - backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: paddingBottom).isActive = true - - if let borderWidth = collectionView.layer?.borderWidth { - backgroundView.layer?.borderWidth = borderWidth + guard collectionElem.getPadding(), collectionElem.getCanBleed() else { return } + + let padding = CGFloat(truncating: hostConfig.getSpacing()?.paddingSpacing ?? 0) + let bleedDirectionValues = ACRBleedDirection(rawValue: collectionElem.getBleedDirection().rawValue).getBleedValues() + + // stackview constraint setup for bleed + collectionView.setStackViewConstraint(direction: bleedDirectionValues) + + // bleed view setup + collectionView.setBleedViewConstraint(direction: bleedDirectionValues, with: padding) + + // background image setup + if collectionElem.getBackgroundImage() != nil { + if let columnView = collectionView as? ACRColumnView { + columnView.bleedBackgroundImage(direction: bleedDirectionValues, with: padding) } - - if let borderColor = collectionView.layer?.borderColor { - backgroundView.layer?.borderColor = borderColor + if let containerView = collectionView as? ACRContainerView { + containerView.bleedBackgroundImage(direction: bleedDirectionValues, with: padding) } } } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnRenderer.swift b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnRenderer.swift index b08927b968..185a2a1644 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnRenderer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnRenderer.swift @@ -32,7 +32,7 @@ class ColumnRenderer: BaseCardElementRendererProtocol { let view = renderer.render(element: element, with: hostConfig, style: style, rootView: rootView, parentView: columnView, inputs: [], config: config) columnView.configureColumnProperties(for: view) BaseCardElementRenderer.shared.updateLayoutForSeparatorAndAlignment(view: view, element: element, parentView: columnView, rootView: rootView, style: style, hostConfig: hostConfig, config: config, isfirstElement: isFirstElement) - BaseCardElementRenderer.shared.configBleed(collectionView: view, parentView: columnView, with: hostConfig, element: element, parentElement: column) + BaseCardElementRenderer.shared.configBleed(for: view, with: hostConfig, element: element) } columnView.configureLayoutAndVisibility(minHeight: column.getMinHeight()) diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnSetRenderer.swift b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnSetRenderer.swift index a926d75f5a..9979d66481 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnSetRenderer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ColumnSetRenderer.swift @@ -16,6 +16,7 @@ class ColumnSetRenderer: BaseCardElementRendererProtocol { } let columnSetView = ACRColumnSetView(style: columnSet.getStyle(), parentStyle: style, hostConfig: hostConfig, renderConfig: config, superview: parentView, needsPadding: columnSet.getPadding()) columnSetView.translatesAutoresizingMaskIntoConstraints = false + columnSetView.bleed = columnSet.getBleed() if columnSet.getSelectAction() != nil { rootView.accessibilityContext?.registerView(columnSetView) } @@ -46,7 +47,7 @@ class ColumnSetRenderer: BaseCardElementRendererProtocol { let columnView = ColumnRenderer.shared.render(element: column, with: hostConfig, style: columnSet.getStyle(), rootView: rootView, parentView: columnSetView, inputs: [], config: config) columnViews.append(columnView) updateColumnSetSeparatorAndAlignment(columnView: columnView, column: column, columnSetView: columnSetView, rootView: rootView, columnSet: columnSet, isfirstElement: isfirstElement, hostConfig: hostConfig) - BaseCardElementRenderer.shared.configBleed(collectionView: columnView, parentView: columnSetView, with: hostConfig, element: column, parentElement: columnSet) + BaseCardElementRenderer.shared.configBleed(for: columnView, with: hostConfig, element: column) } // Add SelectAction diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ContainerRenderer.swift b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ContainerRenderer.swift index 2e204ea5f6..536a0503af 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ContainerRenderer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Renderers/ContainerRenderer.swift @@ -31,7 +31,7 @@ class ContainerRenderer: BaseCardElementRendererProtocol { let renderer = RendererManager.shared.renderer(for: element.getType()) let view = renderer.render(element: element, with: hostConfig, style: container.getStyle(), rootView: rootView, parentView: containerView, inputs: [], config: config) BaseCardElementRenderer.shared.updateLayoutForSeparatorAndAlignment(view: view, element: element, parentView: containerView, rootView: rootView, style: style, hostConfig: hostConfig, config: config, isfirstElement: isFirstElement) - BaseCardElementRenderer.shared.configBleed(collectionView: view, parentView: containerView, with: hostConfig, element: element, parentElement: container) + BaseCardElementRenderer.shared.configBleed(for: view, with: hostConfig, element: element) } containerView.configureLayoutAndVisibility(minHeight: container.getMinHeight()) diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Utils/BleedUtils.swift b/source/macos/AdaptiveCards/AdaptiveCards/Utils/BleedUtils.swift index 7f71e782f2..de0bbbb3ce 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Utils/BleedUtils.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Utils/BleedUtils.swift @@ -1,6 +1,13 @@ import AdaptiveCards_bridge import AppKit +struct ACRBleedValue { + let top: Bool + let bottom: Bool + let leading: Bool + let trailing: Bool +} + struct ACRBleedDirection: OptionSet { let rawValue: UInt @@ -10,4 +17,12 @@ struct ACRBleedDirection: OptionSet { static let ACRBleedToTopEdge = ACRBleedDirection(rawValue: 1 << 2) static let ACRBleedToBottomEdge = ACRBleedDirection(rawValue: 1 << 3) static let ACRBleedToAll: ACRBleedDirection = [.ACRBleedToBottomEdge, .ACRBleedToLeadingEdge, .ACRBleedToTrailingEdge, .ACRBleedToTopEdge] + + func getBleedValues() -> ACRBleedValue { + let top = (self.rawValue & ACRBleedDirection.ACRBleedToTopEdge.rawValue) != 0 + let leading = (self.rawValue & ACRBleedDirection.ACRBleedToLeadingEdge.rawValue) != 0 + let trailing = (self.rawValue & ACRBleedDirection.ACRBleedToTrailingEdge.rawValue) != 0 + let bottom = (self.rawValue & ACRBleedDirection.ACRBleedToBottomEdge.rawValue) != 0 + return ACRBleedValue(top: top, bottom: bottom, leading: leading, trailing: trailing) + } } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnSetView.swift b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnSetView.swift index 3cdffe2829..5fec61852e 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnSetView.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnSetView.swift @@ -85,4 +85,10 @@ class ACRColumnSetView: ACRContentStackView { self.applyVisibilityToSubviews() self.setMinimumHeight(minHeight) } + + override func setBleedViewConstraint(direction: ACRBleedValue, with padding: CGFloat) { + // adding this below stackView + addSubview(bleedView, positioned: .below, relativeTo: self.stackView) + super.setBleedViewConstraint(direction: direction, with: padding) + } } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnView.swift b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnView.swift index db20045857..76cad66374 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnView.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRColumnView.swift @@ -48,10 +48,10 @@ class ACRColumnView: ACRContentStackView { } private lazy var widthConstraint = widthAnchor.constraint(equalToConstant: Constants.minWidth) - private lazy var backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor) - private lazy var backgroundImageViewTopConstraint = backgroundImageView.topAnchor.constraint(equalTo: topAnchor) - private lazy var backgroundImageViewLeadingConstraint = backgroundImageView.leadingAnchor.constraint(equalTo: leadingAnchor) - private lazy var backgroundImageViewTrailingConstraint = backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor) + private (set) lazy var backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor) + private (set) lazy var backgroundImageViewTopConstraint = backgroundImageView.topAnchor.constraint(equalTo: topAnchor) + private (set) lazy var backgroundImageViewLeadingConstraint = backgroundImageView.leadingAnchor.constraint(equalTo: leadingAnchor) + private (set) lazy var backgroundImageViewTrailingConstraint = backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor) private (set) var columnWidth: ColumnWidth = .weighted(1) private (set) lazy var minWidthConstraint: NSLayoutConstraint = { @@ -63,6 +63,7 @@ class ACRColumnView: ACRContentStackView { private (set) lazy var backgroundImageView: ACRBackgroundImageView = { let view = ACRBackgroundImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true return view }() @@ -127,6 +128,7 @@ class ACRColumnView: ACRContentStackView { } func setupBackgroundImageProperties(_ properties: ACSBackgroundImage) { + backgroundImageView.isHidden = false backgroundImageView.fillMode = properties.getFillMode() backgroundImageView.horizontalAlignment = properties.getHorizontalAlignment() backgroundImageView.verticalAlignment = properties.getVerticalAlignment() @@ -174,12 +176,16 @@ class ACRColumnView: ACRContentStackView { } } - func bleedBackgroundImage(padding: CGFloat, top: Bool, bottom: Bool, leading: Bool, trailing: Bool, paddingBottom: CGFloat, with anchor: NSLayoutAnchor) { - backgroundImageViewTopConstraint.constant = top ? -padding : 0 - backgroundImageViewTrailingConstraint.constant = trailing ? padding : 0 - backgroundImageViewLeadingConstraint.constant = leading ? -padding : 0 - backgroundImageViewBottomConstraint.isActive = false - backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: anchor, constant: bottom ? padding : 0) - backgroundImageViewBottomConstraint.isActive = true + func bleedBackgroundImage(direction: ACRBleedValue, with padding: CGFloat) { + backgroundImageViewTopConstraint.constant = direction.top ? -padding : 0 + backgroundImageViewTrailingConstraint.constant = direction.trailing ? padding : 0 + backgroundImageViewLeadingConstraint.constant = direction.leading ? -padding : 0 + backgroundImageViewBottomConstraint.constant = direction.bottom ? padding : 0 + } + + override func setBleedViewConstraint(direction: ACRBleedValue, with padding: CGFloat) { + // adding this below collectionView backgroundImage view + addSubview(bleedView, positioned: .below, relativeTo: self.backgroundImageView) + super.setBleedViewConstraint(direction: direction, with: padding) } } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContainerView.swift b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContainerView.swift index 05abafa102..8237a61946 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContainerView.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContainerView.swift @@ -12,13 +12,14 @@ class ACRContainerView: ACRContentStackView { private (set) lazy var backgroundImageView: ACRBackgroundImageView = { let view = ACRBackgroundImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true return view }() private lazy var widthConstraint = widthAnchor.constraint(equalToConstant: 30) - private lazy var backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor) - private lazy var backgroundImageViewTopConstraint = backgroundImageView.topAnchor.constraint(equalTo: topAnchor) - private lazy var backgroundImageViewLeadingConstraint = backgroundImageView.leadingAnchor.constraint(equalTo: leadingAnchor) - private lazy var backgroundImageViewTrailingConstraint = backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor) + private (set) lazy var backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor) + private (set) lazy var backgroundImageViewTopConstraint = backgroundImageView.topAnchor.constraint(equalTo: topAnchor) + private (set) lazy var backgroundImageViewLeadingConstraint = backgroundImageView.leadingAnchor.constraint(equalTo: leadingAnchor) + private (set) lazy var backgroundImageViewTrailingConstraint = backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor) // AccessibleFocusView property override var validKeyView: NSView? { @@ -99,18 +100,23 @@ class ACRContainerView: ACRContentStackView { } func setupBackgroundImageProperties(_ properties: ACSBackgroundImage) { + backgroundImageView.isHidden = false backgroundImageView.fillMode = properties.getFillMode() backgroundImageView.horizontalAlignment = properties.getHorizontalAlignment() backgroundImageView.verticalAlignment = properties.getVerticalAlignment() heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true } - func bleedBackgroundImage(padding: CGFloat, top: Bool, bottom: Bool, leading: Bool, trailing: Bool, paddingBottom: CGFloat, with anchor: NSLayoutAnchor) { - backgroundImageViewTopConstraint.constant = top ? -padding : 0 - backgroundImageViewTrailingConstraint.constant = trailing ? padding : 0 - backgroundImageViewLeadingConstraint.constant = leading ? -padding : 0 - backgroundImageViewBottomConstraint.isActive = false - backgroundImageViewBottomConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: anchor, constant: bottom ? padding : 0) - backgroundImageViewBottomConstraint.isActive = true + func bleedBackgroundImage(direction: ACRBleedValue, with padding: CGFloat) { + backgroundImageViewTopConstraint.constant = direction.top ? -padding : 0 + backgroundImageViewTrailingConstraint.constant = direction.trailing ? padding : 0 + backgroundImageViewLeadingConstraint.constant = direction.leading ? -padding : 0 + backgroundImageViewBottomConstraint.constant = direction.bottom ? padding : 0 + } + + override func setBleedViewConstraint(direction: ACRBleedValue, with padding: CGFloat) { + // adding this below collectionView backgroundImage view + addSubview(bleedView, positioned: .below, relativeTo: self.backgroundImageView) + super.setBleedViewConstraint(direction: direction, with: padding) } } diff --git a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContentStackView.swift b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContentStackView.swift index 0955edf633..0d608cac08 100644 --- a/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContentStackView.swift +++ b/source/macos/AdaptiveCards/AdaptiveCards/Views/ACRContentStackView.swift @@ -11,11 +11,6 @@ protocol ACRContentHoldingViewProtocol { } class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHandlingProtocol, AccessibleFocusView { - private var stackViewLeadingConstraint: NSLayoutConstraint? - private var stackViewTrailingConstraint: NSLayoutConstraint? - private var stackViewTopConstraint: NSLayoutConstraint? - private var stackViewBottomConstraint: NSLayoutConstraint? - // map table store errror message field private let errorMessageFieldMap = NSMapTable(keyOptions: .strongMemory, valueOptions: .weakMemory) // map table store input label field @@ -80,6 +75,23 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa view.distribution = .fill return view }() + // StackView layout constraint + private (set) var stackViewLeadingLayoutConstraint: NSLayoutConstraint? + private (set) var stackViewTrailingLayoutConstraint: NSLayoutConstraint? + private (set) var stackViewTopLayoutConstraint: NSLayoutConstraint? + private (set) var stackViewBottomLayoutConstraint: NSLayoutConstraint? + + private (set) lazy var bleedView: NSView = { + let view = NSView() + view.translatesAutoresizingMaskIntoConstraints = false + view.wantsLayer = true + return view + }() + // BleedView layout constraint + private (set) var bleedViewLeadingLayoutConstraint: NSLayoutConstraint? + private (set) var bleedViewTrailingLayoutConstraint: NSLayoutConstraint? + private (set) var bleedViewTopLayoutConstraint: NSLayoutConstraint? + private (set) var bleedViewBottomLayoutConstraint: NSLayoutConstraint? var hasStretchableView: Bool { return visibilityManager.fillerSpaceManager.hasPadding() @@ -130,6 +142,9 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa if needsPadding { if let bgColor = hostConfig.getBackgroundColor(for: style) { layer?.backgroundColor = bgColor.cgColor + bleedView.layer?.backgroundColor = self.layer?.backgroundColor + bleedView.layer?.borderWidth = self.layer?.borderWidth ?? 0 + bleedView.layer?.borderColor = self.layer?.borderColor } /* Experimental Feature // set border color @@ -187,27 +202,37 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa } func applyPadding(_ padding: CGFloat) { - stackViewLeadingConstraint?.constant = padding - stackViewTopConstraint?.constant = padding - stackViewTrailingConstraint?.constant = -padding - stackViewBottomConstraint?.constant = -padding + stackViewLeadingLayoutConstraint?.constant = padding + stackViewTopLayoutConstraint?.constant = padding + stackViewTrailingLayoutConstraint?.constant = -padding + stackViewBottomLayoutConstraint?.constant = -padding } - func setBleedProp(top: Bool, bottom: Bool, trailing: Bool, leading: Bool) { - if top { - stackViewTopConstraint?.constant = 0 + func setStackViewConstraint(direction: ACRBleedValue) { + if direction.top { + stackViewTopLayoutConstraint?.constant = 0 } - if bottom { - stackViewBottomConstraint?.constant = 0 + if direction.bottom { + stackViewBottomLayoutConstraint?.constant = 0 } - if leading { - stackViewLeadingConstraint?.constant = 0 + if direction.leading { + stackViewLeadingLayoutConstraint?.constant = 0 } - if trailing { - stackViewTrailingConstraint?.constant = 0 + if direction.trailing { + stackViewTrailingLayoutConstraint?.constant = 0 } } + func setBleedViewConstraint(direction: ACRBleedValue, with padding: CGFloat) { + bleedViewLeadingLayoutConstraint?.constant = direction.leading ? -padding : 0 + bleedViewTopLayoutConstraint?.constant = direction.top ? -padding : 0 + bleedViewTrailingLayoutConstraint?.constant = direction.trailing ? padding : 0 + bleedViewBottomLayoutConstraint?.constant = direction.bottom ? padding : 0 + + guard let leading = bleedViewLeadingLayoutConstraint, let trailing = bleedViewTrailingLayoutConstraint, let top = bleedViewTopLayoutConstraint, let bottom = bleedViewBottomLayoutConstraint else { return } + NSLayoutConstraint.activate([leading, trailing, top, bottom]) + } + func setCustomSpacing(spacing: CGFloat, after view: NSView) { stackView.setCustomSpacing(spacing, after: view) } @@ -396,21 +421,28 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa /// This method can be overridden, but not to be called from anywhere func setupConstraints() { - stackViewLeadingConstraint = stackView.leadingAnchor.constraint(equalTo: leadingAnchor) - stackViewTrailingConstraint = stackView.trailingAnchor.constraint(equalTo: trailingAnchor) - stackViewTopConstraint = stackView.topAnchor.constraint(equalTo: topAnchor) - stackViewBottomConstraint = stackView.bottomAnchor.constraint(equalTo: bottomAnchor) + // StackView constraint + stackViewLeadingLayoutConstraint = stackView.leadingAnchor.constraint(equalTo: leadingAnchor) + stackViewTopLayoutConstraint = stackView.topAnchor.constraint(equalTo: topAnchor) + stackViewTrailingLayoutConstraint = stackView.trailingAnchor.constraint(equalTo: trailingAnchor) + stackViewBottomLayoutConstraint = stackView.bottomAnchor.constraint(equalTo: bottomAnchor) - guard let leading = stackViewLeadingConstraint, let trailing = stackViewTrailingConstraint, let top = stackViewTopConstraint, let bottom = stackViewBottomConstraint else { return } + // BleedView constraint + bleedViewLeadingLayoutConstraint = bleedView.leadingAnchor.constraint(equalTo: leadingAnchor) + bleedViewTopLayoutConstraint = bleedView.topAnchor.constraint(equalTo: topAnchor) + bleedViewTrailingLayoutConstraint = bleedView.trailingAnchor.constraint(equalTo: trailingAnchor) + bleedViewBottomLayoutConstraint = bleedView.bottomAnchor.constraint(equalTo: bottomAnchor) + + guard let leading = stackViewLeadingLayoutConstraint, let trailing = stackViewTrailingLayoutConstraint, let top = stackViewTopLayoutConstraint, let bottom = stackViewBottomLayoutConstraint else { return } NSLayoutConstraint.activate([leading, trailing, top, bottom]) stackView.setContentHuggingPriority(.defaultLow, for: .vertical) } /// This method can be overridden, but not to be called from anywhere func anchorBottomConstraint(with anchor: NSLayoutAnchor) { - stackViewBottomConstraint?.isActive = false - stackViewBottomConstraint = stackView.bottomAnchor.constraint(equalTo: anchor) - stackViewBottomConstraint?.isActive = true + stackViewBottomLayoutConstraint?.isActive = false + stackViewBottomLayoutConstraint = stackView.bottomAnchor.constraint(equalTo: anchor) + stackViewBottomLayoutConstraint?.isActive = true } /// This methid can be overridden. super implementation must be called @@ -456,16 +488,6 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa setInputLabel(isHidden: false, for: inputView) } - // MARK: Mouse Events and SelectAction logics - private var previousBackgroundColor: CGColor? - override func mouseEntered(with event: NSEvent) { - guard target != nil else { return } - previousBackgroundColor = layer?.backgroundColor - layer?.backgroundColor = ColorUtils.hoverColorOnMouseEnter().cgColor - // Added a pointing hand here - self.setCursorType(cursor: .pointingHand) - } - /// set the cursor type while the image hovering and the select action is active /// - Parameter cursor: accept cursor type private func setCursorType(cursor: NSCursor) { @@ -530,9 +552,24 @@ class ACRContentStackView: NSView, ACRContentHoldingViewProtocol, SelectActionHa inputLabelField?.isHidden = hidden } + // MARK: Mouse Events and SelectAction logics + private var previousBackgroundColor: CGColor? + + override func mouseEntered(with event: NSEvent) { + guard target != nil else { return } + previousBackgroundColor = layer?.backgroundColor + layer?.backgroundColor = ColorUtils.hoverColorOnMouseEnter().cgColor + // bleedView + bleedView.layer?.backgroundColor = self.layer?.backgroundColor + // Added a pointing hand here + self.setCursorType(cursor: .pointingHand) + } + override func mouseExited(with event: NSEvent) { guard target != nil else { return } layer?.backgroundColor = previousBackgroundColor ?? .clear + // bleedView + bleedView.layer?.backgroundColor = self.layer?.backgroundColor // Back to the system cursor self.setCursorType(cursor: .current) } diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumn.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumn.swift index 117b8dc4f4..3c65729f83 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumn.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumn.swift @@ -82,6 +82,14 @@ class FakeColumn: ACSColumn { bleed = value } + override func getCanBleed() -> Bool { + return true + } + + override func getBleedDirection() -> ACSContainerBleedDirection { + return .bleedAll + } + override func getSpacing() -> ACSSpacing { return spacing } @@ -131,6 +139,7 @@ extension FakeColumn { fakeColumn.verticalContentAlignment = verticalContentAlignment fakeColumn.minHeight = minHeight fakeColumn.bleed = bleed + fakeColumn.padding = padding fakeColumn.spacing = spacing fakeColumn.height = height fakeColumn.separator = separator diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumnSet.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumnSet.swift index 97dc96f235..ec8fbd9dca 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumnSet.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeColumnSet.swift @@ -12,7 +12,6 @@ class FakeColumnSet: ACSColumnSet { var height: ACSHeightType = .auto var type: ACSCardElementType = .columnSet var bleed: Bool = false - var backgroundImage: ACSBackgroundImage? var separator: Bool = false var visible: Bool = true @@ -60,6 +59,10 @@ class FakeColumnSet: ACSColumnSet { return padding } + override func setPadding(_ value: Bool) { + padding = value + } + override func getMinHeight() -> NSNumber? { return minHeight } @@ -84,12 +87,12 @@ class FakeColumnSet: ACSColumnSet { bleed = value } - open override func getBackgroundImage() -> ACSBackgroundImage? { - return backgroundImage + override func getCanBleed() -> Bool { + return true } - open override func setBackgroundImage(_ value: ACSBackgroundImage?) { - backgroundImage = value + override func getBleedDirection() -> ACSContainerBleedDirection { + return .bleedAll } override func getSeparator() -> Bool { @@ -99,10 +102,14 @@ class FakeColumnSet: ACSColumnSet { override func getIsVisible() -> Bool { return visible } + + override func getBackgroundImage() -> ACSBackgroundImage? { + return nil + } } extension FakeColumnSet { - static func make(columns: [ACSColumn] = [], style: ACSContainerStyle = .default, selectAction: ACSBaseActionElement? = nil, horizontalAlignment: ACSHorizontalAlignment = .left, padding: Bool = false, heightType: ACSHeightType = .auto) -> FakeColumnSet { + static func make(columns: [ACSColumn] = [], style: ACSContainerStyle = .default, selectAction: ACSBaseActionElement? = nil, horizontalAlignment: ACSHorizontalAlignment = .left, bleed: Bool = false, padding: Bool = false, heightType: ACSHeightType = .auto) -> FakeColumnSet { let fakeColumnSet = FakeColumnSet() fakeColumnSet.columns = columns fakeColumnSet.style = style @@ -110,6 +117,7 @@ extension FakeColumnSet { fakeColumnSet.horizontalAlignment = horizontalAlignment fakeColumnSet.padding = padding fakeColumnSet.height = heightType + fakeColumnSet.bleed = bleed return fakeColumnSet } } diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeContainer.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeContainer.swift index 912e8c94ba..a168d19b91 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeContainer.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Fakes/FakeContainer.swift @@ -43,6 +43,14 @@ class FakeContainer: ACSContainer { bleed = value } + override func getCanBleed() -> Bool { + return true + } + + override func getBleedDirection() -> ACSContainerBleedDirection { + return .bleedAll + } + open override func getBackgroundImage() -> ACSBackgroundImage? { return backgroundImage } diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnRendererTests.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnRendererTests.swift index 557a2d1987..c8fec8d3c7 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnRendererTests.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnRendererTests.swift @@ -63,6 +63,36 @@ class ColumnRendererTests: XCTestCase { XCTAssertEqual(columnView.arrangedSubviews.count, 2) } + func testBleedView() { + let backgroundImage = FakeBackgroundImage.make(url: "https://picsum.photos/200", fillMode: .cover) + hostConfig = .make(spacing: ACSSpacingConfig.init(smallSpacing: 4, defaultSpacing: 6, mediumSpacing: 8, largeSpacing: 10, extraLargeSpacing: 14, paddingSpacing: 12)) + + let column = FakeColumn.make(bleed: true, backgroundImage: backgroundImage, padding: true) + let columnView = renderColumnView(column) + + XCTAssertFalse(columnView.bleedViewTopLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnView.bleedViewBottomLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnView.bleedViewLeadingLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnView.bleedViewTrailingLayoutConstraint?.isActive ?? true) + + BaseCardElementRenderer.shared.configBleed(for: columnView, with: hostConfig, element: column) + + XCTAssertEqual(columnView.bleedViewTopLayoutConstraint?.constant, -12) + XCTAssertEqual(columnView.bleedViewBottomLayoutConstraint?.constant, 12) + XCTAssertEqual(columnView.bleedViewLeadingLayoutConstraint?.constant, -12) + XCTAssertEqual(columnView.bleedViewTrailingLayoutConstraint?.constant, 12) + + XCTAssertEqual(columnView.backgroundImageViewTopConstraint.constant, -12) + XCTAssertEqual(columnView.backgroundImageViewBottomConstraint.constant, 12) + XCTAssertEqual(columnView.backgroundImageViewLeadingConstraint.constant, -12) + XCTAssertEqual(columnView.backgroundImageViewTrailingConstraint.constant, 12) + + XCTAssertEqual(columnView.stackViewTopLayoutConstraint?.constant, 0) + XCTAssertEqual(columnView.stackViewBottomLayoutConstraint?.constant, 0) + XCTAssertEqual(columnView.stackViewLeadingLayoutConstraint?.constant, 0) + XCTAssertEqual(columnView.stackViewTrailingLayoutConstraint?.constant, 0) + } + func testSelectActionTargetIsSet() { var columnView: ACRColumnView! diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnSetRendererTests.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnSetRendererTests.swift index 393dcf6dc8..b457c9f297 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnSetRendererTests.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ColumnSetRendererTests.swift @@ -63,6 +63,30 @@ class ColumnSetRendererTests: XCTestCase { XCTAssertEqual(columnSetView.distribution, .fill) } + func testBleedView() { + hostConfig = .make(spacing: ACSSpacingConfig.init(smallSpacing: 4, defaultSpacing: 6, mediumSpacing: 8, largeSpacing: 10, extraLargeSpacing: 14, paddingSpacing: 12)) + columnSet = .make(columns: [FakeColumn.make(), FakeColumn.make()], bleed: true, padding: true) + + let columnSetView = renderColumnSetView() + + XCTAssertFalse(columnSetView.bleedViewTopLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnSetView.bleedViewBottomLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnSetView.bleedViewLeadingLayoutConstraint?.isActive ?? true) + XCTAssertFalse(columnSetView.bleedViewTrailingLayoutConstraint?.isActive ?? true) + + BaseCardElementRenderer.shared.configBleed(for: columnSetView, with: hostConfig, element: columnSet) + + XCTAssertEqual(columnSetView.bleedViewTopLayoutConstraint?.constant, -12) + XCTAssertEqual(columnSetView.bleedViewBottomLayoutConstraint?.constant, 12) + XCTAssertEqual(columnSetView.bleedViewLeadingLayoutConstraint?.constant, -12) + XCTAssertEqual(columnSetView.bleedViewTrailingLayoutConstraint?.constant, 12) + + XCTAssertEqual(columnSetView.stackViewTopLayoutConstraint?.constant, 0) + XCTAssertEqual(columnSetView.stackViewBottomLayoutConstraint?.constant, 0) + XCTAssertEqual(columnSetView.stackViewLeadingLayoutConstraint?.constant, 0) + XCTAssertEqual(columnSetView.stackViewTrailingLayoutConstraint?.constant, 0) + } + func testDefaultOrientation() { let columnStackView = renderColumnSetView() XCTAssertEqual(columnStackView.orientation, .horizontal) diff --git a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ContainerRendererTests.swift b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ContainerRendererTests.swift index a6cb040c07..72d839f725 100644 --- a/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ContainerRendererTests.swift +++ b/source/macos/AdaptiveCards/AdaptiveCardsTests/Renderers/ContainerRendererTests.swift @@ -37,6 +37,36 @@ class ContainerRendererTests: XCTestCase { XCTAssertEqual(rootStretchView.stackView.arrangedSubviews.first?.contentHuggingPriority(for: .vertical), kFillerViewLayoutConstraintPriority) } + func testBleedView() { + let backgroundImage = FakeBackgroundImage.make(url: "https://picsum.photos/200", fillMode: .cover) + hostConfig = .make(spacing: ACSSpacingConfig.init(smallSpacing: 4, defaultSpacing: 6, mediumSpacing: 8, largeSpacing: 10, extraLargeSpacing: 14, paddingSpacing: 12)) + + let container = FakeContainer.make(bleed: true, backgroundImage: backgroundImage, padding: true) + let containerView = renderContainerView(container) + + XCTAssertFalse(containerView.bleedViewTopLayoutConstraint?.isActive ?? true) + XCTAssertFalse(containerView.bleedViewBottomLayoutConstraint?.isActive ?? true) + XCTAssertFalse(containerView.bleedViewLeadingLayoutConstraint?.isActive ?? true) + XCTAssertFalse(containerView.bleedViewTrailingLayoutConstraint?.isActive ?? true) + + BaseCardElementRenderer.shared.configBleed(for: containerView, with: hostConfig, element: container) + + XCTAssertEqual(containerView.bleedViewTopLayoutConstraint?.constant, -12) + XCTAssertEqual(containerView.bleedViewBottomLayoutConstraint?.constant, 12) + XCTAssertEqual(containerView.bleedViewLeadingLayoutConstraint?.constant, -12) + XCTAssertEqual(containerView.bleedViewTrailingLayoutConstraint?.constant, 12) + + XCTAssertEqual(containerView.backgroundImageViewTopConstraint.constant, -12) + XCTAssertEqual(containerView.backgroundImageViewBottomConstraint.constant, 12) + XCTAssertEqual(containerView.backgroundImageViewLeadingConstraint.constant, -12) + XCTAssertEqual(containerView.backgroundImageViewTrailingConstraint.constant, 12) + + XCTAssertEqual(containerView.stackViewTopLayoutConstraint?.constant, 0) + XCTAssertEqual(containerView.stackViewBottomLayoutConstraint?.constant, 0) + XCTAssertEqual(containerView.stackViewLeadingLayoutConstraint?.constant, 0) + XCTAssertEqual(containerView.stackViewTrailingLayoutConstraint?.constant, 0) + } + func testRendererSetsVerticalContentAlignment() { var container = FakeContainer.make(verticalContentAlignment: .top, items: [FakeTextBlock.make()]) // Removing this since we don't need subviews to have padding to have explicit width