Skip to content
Merged
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
169 changes: 169 additions & 0 deletions docs/specs/components/List/ListItem.swift.spec.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/native/apple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ apple/
│ │ ├── Icon/ # Icon
│ │ ├── Avatar/ # Avatar
│ │ ├── Item/ # Item
│ │ ├── List/ # ListItem
│ │ ├── Text/ # Text
│ │ ├── Heading/ # Heading
│ │ ├── Button/ # Button
Expand Down
106 changes: 99 additions & 7 deletions packages/native/apple/Sources/Astryx/Item/Item.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public struct Item<Marker: View, StartContent: View, EndContent: View>: View {
let startContent: StartContent
let endContent: EndContent
@Environment(\.astryx) private var astryx
@Environment(\.astryxItemListTreatment) private var listTreatment
@State private var wordsHeight: CGFloat = 0

/// A row from its labeled props. A single trailing closure is the start
/// content; set `onClick` by label or as a trailing `onClick:` closure.
Expand Down Expand Up @@ -131,29 +133,31 @@ public struct Item<Marker: View, StartContent: View, EndContent: View>: View {
/// A link or button row's fill and its press, painted by the button style
/// so the press sits above a selected or highlighted fill, as core's does.
private var pressStyle: ItemPressStyle {
ItemPressStyle(radius: astryx.tokens.radius.element, fill: background, pressed: astryx.tokens.color.overlayPressed)
ItemPressStyle(radius: cornerRadius, fill: background, pressed: astryx.tokens.color.overlayPressed)
}

/// The row's drawing; a tappable row is at least `minHeight` tall. A static
/// row paints its own fill; a link or button row's style paints it.
private func row(minHeight: CGFloat?, paintsFill: Bool) -> some View {
HStack(alignment: rowAlignment, spacing: astryx.tokens.space.s2) {
slot(marker)
slot(startContent)
markerSlot
// A list's treatment is for this row only, not for rows nested in its slots.
slot(startContent.modifier(ItemSlotOutsideList()))
// A disabled row dims its words and end content, as core does; the
// marker and start content keep their own look (a checkbox shows its
// own disabled state).
words
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(ItemWordsHeight(isMeasured: measuresWords, height: $wordsHeight))
.opacity(isDisabled ? 0.5 : 1)
slot(endContent)
slot(endContent.modifier(ItemSlotOutsideList()))
.opacity(isDisabled ? 0.5 : 1)
}
.padding(.horizontal, inlinePadding)
.padding(.vertical, blockPadding)
.frame(maxWidth: .infinity, minHeight: minHeight, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: astryx.tokens.radius.element, style: .continuous)
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(paintsFill ? background ?? .clear : .clear)
)
.contentShape(Rectangle())
Expand All @@ -170,6 +174,43 @@ public struct Item<Marker: View, StartContent: View, EndContent: View>: View {
}
}

/// The marker. Inside a list it centers on the label's first line,
/// whatever the row's alignment, as core's list markers do.
@ViewBuilder private var markerSlot: some View {
if listTreatment?.markerOnFirstLine == true {
let center = firstLineBoxHeight / 2
switch resolvedAlign {
case .firstLine:
slot(marker)
case .start:
marker.alignmentGuide(.top) { d in d.height / 2 - center }
case .center:
let words = wordsHeight
marker.alignmentGuide(VerticalAlignment.center) { d in words / 2 - center + d.height / 2 }
}
} else {
slot(marker)
}
}

/// Whether the row measures its words: only a centre-aligned row in a list
/// with markers, which places its marker from the words' height.
private var measuresWords: Bool {
listTreatment?.markerOnFirstLine == true && resolvedAlign == .center
}

/// The height of the label's first line box: the theme's leading at the
/// current text size.
private var firstLineBoxHeight: CGFloat {
astryx.tokens.font.body.leading * astryx.tokens.font.body.size * astryx.textScale
}

/// The row's corner radius: the theme's element radius, or square inside a
/// list with dividers.
private var cornerRadius: CGFloat {
listTreatment?.squareCorners == true ? 0 : astryx.tokens.radius.element
}

@ViewBuilder private var words: some View {
let label = SwiftUI.Text(props.label)
.font(astryx.font(size: "text-body-size", 14, weight: "text-body-weight"))
Expand Down Expand Up @@ -237,9 +278,15 @@ public struct Item<Marker: View, StartContent: View, EndContent: View>: View {
}
}

/// Inline padding: spacing 2, or 3 when spacious.
/// Inline padding: the list's own inset when a list sets one, else core's
/// for the density.
var inlinePadding: CGFloat {
(props.density ?? .balanced) == .spacious ? astryx.tokens.space.s3 : astryx.tokens.space.s2
listTreatment?.inlineInset ?? Item.inlinePadding(density: props.density, tokens: astryx.tokens)
}

/// Core's inline padding for a density: spacing 2, or 3 when spacious.
static func inlinePadding(density: ItemDensity?, tokens: Tokens) -> CGFloat {
(density ?? .balanced) == .spacious ? tokens.space.s3 : tokens.space.s2
}

/// The space core's line height adds to the font's own line, grown with
Expand Down Expand Up @@ -352,3 +399,48 @@ private struct ItemAccessibility: ViewModifier {
content.accessibilityAddTraits(Item<EmptyView, EmptyView, EmptyView>.addedTraits(isSelected: isSelected))
}
}

/// Measures the words' height only when the row needs it.
private struct ItemWordsHeight: ViewModifier {
let isMeasured: Bool
@Binding var height: CGFloat

func body(content: Content) -> some View {
if isMeasured {
content.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { height = $0 }
} else {
content
}
}
}

/// A row's slots sit outside its list: a row nested in a slot takes neither
/// the list's treatment, its look, nor a number from it.
private struct ItemSlotOutsideList: ViewModifier {
func body(content: Content) -> some View {
content
.environment(\.astryxItemListTreatment, nil)
.environment(\.astryxListContext, nil)
.environment(\.astryxListNumbers, nil)
}
}

/// How a list draws the rows it holds: the marker on the first line, its own
/// inline inset (zero for a flush list), and square corners between dividers.
struct ItemListTreatment: Equatable {
var markerOnFirstLine: Bool
var inlineInset: CGFloat?
var squareCorners: Bool
}

private struct ItemListTreatmentKey: EnvironmentKey {
static let defaultValue: ItemListTreatment? = nil
}

extension EnvironmentValues {
/// Set by `ListItem` on the `Item` it draws; `nil` for an `Item` alone.
var astryxItemListTreatment: ItemListTreatment? {
get { self[ItemListTreatmentKey.self] }
set { self[ItemListTreatmentKey.self] = newValue }
}
}
Loading
Loading