Align side column page content and tabs with the grabber inset

The corner layout side column puts a 12pt resize grabber strip before the
panel, and every page then added the standard 16pt horizontal padding, so
content sat ~28pt from the left edge but 16pt from the right.

Add a PanelHorizontalInsets environment value with a panelHorizontalPadding()
modifier that the info, comments, related and chapters pages use instead of
.padding(.horizontal). The side column trims the leading inset by the grabber
width so content is 16pt from the column's outer edge on both sides; other
hosts keep the defaults. Queue and tracks are List-backed and get the same
leading delta as a scroll content margin. The page bar follows the same insets
so the first pill lines up with the content.
This commit is contained in:
Arkadiusz Fal
2026-09-24 22:01:01 +02:00
parent 7f672d2c57
commit bfbb729a8c
9 changed files with 97 additions and 15 deletions

View File

@@ -60,6 +60,10 @@ struct CornerPlayerLayout<PlayerContent: View>: View {
/// Side column width bounds.
static var minColumnWidth: CGFloat { 280 }
/// Resize grabber strip on the side column's leading edge — narrow,
/// matching the panel drag handle look.
static var grabberWidth: CGFloat { 12 }
static var maxColumnWidthCap: CGFloat { 560 }
/// Minimum width kept for the video column.
static var minVideoColumnWidth: CGFloat { 480 }
@@ -167,7 +171,7 @@ struct CornerPlayerLayout<PlayerContent: View>: View {
let effectiveColumnWidth = clampColumnWidth(columnWidth, availableWidth: width)
// Grabber sits on the column's leading edge, inside the column
// background — narrow strip matching the panel drag handle look.
let grabberWidth: CGFloat = 12
let grabberWidth = Self.grabberWidth
let totalColumnWidth = effectiveColumnWidth + grabberWidth
let videoWidth = showsSideColumn ? max(1, width - totalColumnWidth) : width
@@ -540,6 +544,13 @@ struct CornerPlayerLayout<PlayerContent: View>: View {
pageBarTopPadding: 8,
excludedPages: belowVideoPageTypes
)
// The grabber strip already separates the column from the video;
// trim the leading inset so content sits 16pt from the column's
// outer edge on both sides instead of grabber + 16 on the left.
.environment(\.panelHorizontalInsets, PanelHorizontalInsets(
leading: max(0, PanelHorizontalInsets.standard - Self.grabberWidth),
trailing: PanelHorizontalInsets.standard
))
.background(panelBackgroundColor.ignoresSafeArea(edges: .bottom))
}

View File

@@ -38,7 +38,7 @@ struct ChaptersPanelPage: View {
)
}
}
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.top, 8)
}
.task(id: context.video.id) {

View File

@@ -23,7 +23,7 @@ struct CommentsPanelPage: View {
videoID: context.video.id.videoID,
showAvatars: settings.effectiveShowAvatars
)
.padding(.horizontal)
.panelHorizontalPadding()
}
.task(id: context.video.id) {
// Opening the page is an explicit request; load even when the

View File

@@ -64,7 +64,7 @@ struct InfoPanelPage: View {
if settings.effectiveShowDescription {
Divider()
.padding(.horizontal)
.panelHorizontalPadding()
descriptionContent
.padding(.vertical)
@@ -126,7 +126,7 @@ struct InfoPanelPage: View {
)
}
}
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.vertical, 8)
}
@@ -144,7 +144,7 @@ struct InfoPanelPage: View {
.font(.subheadline)
.foregroundStyle(.secondary)
.tint(context.accentColor)
.padding(.horizontal)
.panelHorizontalPadding()
.handleTimestampLinks(using: playerService)
}
} else if videoDetailsState == .loading {
@@ -158,7 +158,7 @@ struct InfoPanelPage: View {
Text(String(localized: "player.noDescription"))
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.horizontal)
.panelHorizontalPadding()
}
}
}
@@ -221,7 +221,7 @@ struct InfoPanelPage: View {
.buttonStyle(.plain)
}
}
.padding(.horizontal)
.panelHorizontalPadding()
.contentShape(Rectangle())
.onTapGesture {
if !isDescriptionExpanded {
@@ -257,7 +257,7 @@ struct InfoPanelPage: View {
Spacer(minLength: 0)
}
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.top, 4)
.padding(.bottom, 8)
.contentShape(Rectangle())
@@ -269,7 +269,7 @@ struct InfoPanelPage: View {
private var chaptersSection: some View {
if !chapters.isEmpty {
Divider()
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.bottom, 8)
sectionHeader(String(localized: "player.chapters"), isCollapsed: $isChaptersCollapsed)
@@ -290,7 +290,7 @@ struct InfoPanelPage: View {
)
}
}
.padding(.horizontal)
.panelHorizontalPadding()
}
}
}
@@ -299,7 +299,7 @@ struct InfoPanelPage: View {
private var relatedSection: some View {
if !relatedVideos.isEmpty {
Divider()
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.vertical, 8)
sectionHeader(String(localized: "videoInfo.section.relatedVideos"), isCollapsed: $isRelatedCollapsed)
@@ -327,7 +327,7 @@ struct InfoPanelPage: View {
}
}
}
.padding(.horizontal)
.panelHorizontalPadding()
}
}
}

View File

@@ -15,6 +15,13 @@ struct PanelPageBar: View {
let accentColor: Color
var showLabels: Bool = true
@Environment(\.panelHorizontalInsets) private var insets
/// Pills sit 4pt closer to the edge than page content (12 vs 16 with the
/// standard inset); a host that trims the content inset (corner side
/// column) trims the bar the same way so the first pill lines up.
private static let pillEdgeOffset: CGFloat = 4
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
@@ -24,7 +31,8 @@ struct PanelPageBar: View {
.id(page.pageType)
}
}
.padding(.horizontal, 12)
.padding(.leading, max(0, insets.leading - Self.pillEdgeOffset))
.padding(.trailing, max(0, insets.trailing - Self.pillEdgeOffset))
.padding(.vertical, 6)
}
.onChange(of: selectedPage) { _, newValue in

View File

@@ -46,6 +46,67 @@ struct PanelScrollConfig {
var onOverscrollDragEnded: ((CGFloat, CGFloat) -> Void)?
}
// MARK: - Horizontal Insets
/// Horizontal content insets a host wants on the panel pages. Hosts whose
/// panel already carries leading chrome (the corner side column's resize
/// grabber strip) narrow the leading inset so content sits the same distance
/// from both edges.
struct PanelHorizontalInsets: Equatable {
static let standard: CGFloat = 16
var leading: CGFloat = PanelHorizontalInsets.standard
var trailing: CGFloat = PanelHorizontalInsets.standard
/// Leading delta relative to the standard inset; List-backed pages
/// (queue, tracks) apply it as a scroll content margin since their
/// section insets can't be restyled per host.
var leadingListAdjustment: CGFloat { leading - Self.standard }
}
private struct PanelHorizontalInsetsKey: EnvironmentKey {
static let defaultValue = PanelHorizontalInsets()
}
extension EnvironmentValues {
var panelHorizontalInsets: PanelHorizontalInsets {
get { self[PanelHorizontalInsetsKey.self] }
set { self[PanelHorizontalInsetsKey.self] = newValue }
}
}
private struct PanelHorizontalPadding: ViewModifier {
@Environment(\.panelHorizontalInsets) private var insets
func body(content: Content) -> some View {
content
.padding(.leading, insets.leading)
.padding(.trailing, insets.trailing)
}
}
private struct PanelListLeadingMargin: ViewModifier {
@Environment(\.panelHorizontalInsets) private var insets
func body(content: Content) -> some View {
content
.contentMargins(.leading, insets.leadingListAdjustment, for: .scrollContent)
}
}
extension View {
/// Host-configurable replacement for `.padding(.horizontal)` on panel
/// page content.
func panelHorizontalPadding() -> some View {
modifier(PanelHorizontalPadding())
}
/// Shifts List-backed page content by the host's leading inset delta.
func panelListLeadingMargin() -> some View {
modifier(PanelListLeadingMargin())
}
}
// MARK: - Page Scroll View
/// Shared scroll container used by the scrollable panel pages. Applies the

View File

@@ -17,6 +17,7 @@ struct QueuePanelPage: View {
var body: some View {
QueueListView(showHistory: settings.effectiveShowHistory)
.safeAreaPadding(.bottom, bottomInset)
.panelListLeadingMargin()
}
}

View File

@@ -54,7 +54,7 @@ struct RelatedPanelPage: View {
}
}
}
.padding(.horizontal)
.panelHorizontalPadding()
.padding(.top, 8)
}
}

View File

@@ -89,6 +89,7 @@ struct TracksPanelPage: View {
visibleSections: settings
)
.safeAreaPadding(.bottom, bottomInset)
.panelListLeadingMargin()
}
}
}