Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 31 additions & 23 deletions Sources/Luminare/Components/Composes/LuminarePickerMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import SwiftUI
// MARK: - Picker Menu (Compose)

public struct LuminarePickerMenu<Label, Option, Item>: View
where Label: View, Option: View, Item: Hashable {
where Label: View, Option: View, Item: Hashable {
@Environment(\.luminareSectionHorizontalPadding) private var horizontalPadding

// MARK: Fields

@ViewBuilder private var label: () -> Label
@Binding private var selection: Item
let items: [Item]
@ViewBuilder private var itemToView: (Item) -> Option

// MARK: Initializers

public init(
selection: Binding<Item>,
items: [Item],
Expand Down Expand Up @@ -64,31 +64,40 @@ where Label: View, Option: View, Item: Hashable {
Text(titleKey)
}
}

// MARK: Body

public var body: some View {
LuminareCompose {
HStack(spacing: 8) {
itemToView(selection)

Image(systemName: "chevron.up.chevron.down")
.font(.caption2)
.padding(4)
.luminareSurface()
.luminareCornerRadius(8)
}
.overlay {
Picker("", selection: $selection) {
ForEach(items, id: \.self) { item in
Menu {
ForEach(items, id: \.self) { item in
Toggle(isOn: Binding(
get: { selection == item },
set: { isSelected in
if isSelected {
selection = item
}
}
)) {
itemToView(item)
.tag(item)
}
}
.opacity(0.001)
.fixedSize()
} label: {
HStack(spacing: 8) {
itemToView(selection)

Image(systemName: "chevron.up.chevron.down")
.font(.caption2)
.padding(4)
.luminareSurface()
.luminareCornerRadius(8)
}
.contentShape(.rect)
}
.menuStyle(.button)
.buttonStyle(.plain)
.menuIndicator(.hidden)
.fixedSize()
} label: {
label()
}
Expand Down Expand Up @@ -116,4 +125,3 @@ where Label: View, Option: View, Item: Hashable {
}
.frame(width: 300)
}

128 changes: 79 additions & 49 deletions Sources/Luminare/Components/LuminareCompactPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
// MARK: Environments

@Environment(\.luminareCompactPickerStyle) private var style
@Environment(\.luminareMinHeight) private var minHeight

// MARK: Fields

Expand Down Expand Up @@ -64,6 +65,9 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
.pickerStyle(.menu)
.buttonStyle(.borderless)
.padding(.trailing, -2)
.frame(minHeight: minHeight)
.luminareSurface(isHovering: isHovering, style: .flat)
.luminareFilledStates(.all)
case .segmented:
UnaryVariadicView(content()) { children in
SegmentedVariadic(
Expand All @@ -73,9 +77,9 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
)
}
.padding(.horizontal, 4)
.luminareSurface(style: .flat)
}
}
.luminareSurface(style: .flat)
.onHover { isHovering = $0 }
}

Expand All @@ -93,32 +97,79 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
@Namespace private var namespace

var body: some View {
HStack(spacing: 4) {
ForEach(Array(children.enumerated()), id: \.offset) { index, child in
if let value = child.id(as: V.self) {
SegmentedKnob(
child: child,
namespace: namespace,
isParentHovering: isHovering,
selection: $selection,
value: value,
index: index,
maxIndex: children.count - 1
)
.foregroundStyle(isHovering && selection == value ? .primary : .secondary)
.zIndex(1)

if hasDividers,
child.id != children.last?.id {
Divider()
.frame(height: minHeight / 2)
.zIndex(0)
ProposedHeightLayout(minimumHeight: minHeight) {
HStack(spacing: 4) {
ForEach(Array(children.enumerated()), id: \.offset) { index, child in
if let value = child.id(as: V.self) {
let nextValue: V? = if index < children.count - 1 {
children[index + 1].id(as: V.self)
} else {
nil
}
SegmentedKnob(
child: child,
namespace: namespace,
isParentHovering: isHovering,
selection: $selection,
value: value,
index: index,
maxIndex: children.count - 1
)
.foregroundStyle(selection == value ? .primary : .secondary)
.zIndex(1)

if hasDividers,
child.id != children.last?.id {
Divider()
.frame(height: minHeight / 2)
.opacity(selection == value || selection == nextValue ? 0 : 1)
.zIndex(0)
}
}
}
}
}
}

struct ProposedHeightLayout: Layout {
let minimumHeight: CGFloat

func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache _: inout ()
) -> CGSize {
guard let subview = subviews.first else {
return .zero
}

let proposedWidth = proposal.width.flatMap { $0.isFinite ? $0 : nil }
let proposedHeight = proposal.height.flatMap { $0.isFinite ? $0 : nil }
let size = subview.sizeThatFits(.init(
width: proposedWidth,
height: proposedHeight
))

return .init(
width: proposedWidth ?? size.width,
height: max(minimumHeight, proposedHeight ?? size.height)
)
}

func placeSubviews(
in bounds: CGRect,
proposal _: ProposedViewSize,
subviews: Subviews,
cache _: inout ()
) {
subviews.first?.place(
at: bounds.origin,
anchor: .topLeading,
proposal: .init(bounds.size)
)
}
}

struct SegmentedKnob: View {
@Environment(\.luminareAnimation) private var animation
@Environment(\.luminareMinHeight) private var minHeight
Expand All @@ -143,10 +194,11 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
}
} label: {
child
.frame(maxWidth: .infinity, minHeight: minHeight - 8)
.padding(.horizontal, 8)
.frame(maxWidth: .infinity, minHeight: minHeight - 8, maxHeight: .infinity)
.padding(.horizontal, 12)
}
.buttonStyle(.borderless)
.frame(minHeight: minHeight - 8, maxHeight: .infinity)
.onHover { isHovering = $0 }
.background {
Group {
Expand All @@ -162,7 +214,7 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
}
}
.padding(.vertical, 4)
.frame(minHeight: minHeight)
.frame(minHeight: minHeight, maxHeight: .infinity)
}

private var constrainedCornerRadii: RectangleCornerRadii {
Expand All @@ -181,24 +233,8 @@ public struct LuminareCompactPicker<Content, V>: View where Content: View, V: Ha
}

private func knob() -> some View {
Group {
if isParentHovering {
Rectangle()
.foregroundStyle(.background.opacity(0.8))
} else {
// The `.blendMode()` prevents `.quinary` style to be clipped
Rectangle()
.foregroundStyle(.quinary.blendMode(.luminosity))
}
}
.overlay {
if isHovering {
Rectangle()
.foregroundStyle(.background.opacity(0.2))
.blendMode(.luminosity)
}
}
.clipShape(.rect(cornerRadii: constrainedCornerRadii))
UnevenRoundedRectangle(cornerRadii: constrainedCornerRadii)
.foregroundStyle(isHovering ? .quaternary : .quinary)
}
}
}
Expand All @@ -213,7 +249,7 @@ private struct PickerPreview<V>: View where V: Hashable & Equatable {
var body: some View {
LuminareCompactPicker(selection: $selection) {
ForEach(elements, id: \.self) { element in
Text("\(element)")
Text(verbatim: "\(element)")
}
}
}
Expand All @@ -225,12 +261,6 @@ private struct PickerPreview<V>: View where V: Hashable & Equatable {
traits: .sizeThatFitsLayout
) {
LuminareSection {
LuminareCompose("Button") {
Button {} label: {
Text("42")
}
}

LuminareCompose("Pick from a menu") {
PickerPreview(elements: Array(0 ..< 200), selection: 42)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
@Row {
@Column(size: 3) {
```swift
LuminareCompactPicker(selection: $selection, style: .menu) {
LuminareCompactPicker(selection: $selection) {
ForEach(data, id: \.keyPath) { element in
// Content
}
}
.luminareCompactPickerStyle(.menu)
```
}

Expand All @@ -31,14 +32,15 @@
@Row {
@Column(size: 2) {
```swift
LuminareCompactPicker(selection: $selection, style: .segmented) {
LuminareCompactPicker(selection: $selection) {
ForEach(data, id: \.self) { element in
Group {
// Content
}
.id(element)
}
}
.luminareCompactPickerStyle(.segmented)
```
}

Expand Down
Loading