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
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ struct ArtifactorySection: View {
/// the same thing — a fact about a person at a screen, never about the loop.
let seenPostID: Int?
let isFolded: Bool
/// How tall the scroll box may grow before it scrolls — the rail's share for the
/// board (`LoopWorkspaceRail.artifactoryHeightCap`), never more than
/// `maxScrollHeight`.
var maxHeight: CGFloat = ArtifactorySection.maxScrollHeight
let onToggleFold: () -> Void
/// Posts as "a human" — a click in the app has no `ZMX_SESSION` and no loop identity,
/// which is exactly what a person talking to the whole graph is.
Expand All @@ -58,11 +62,10 @@ struct ArtifactorySection: View {
/// Whether the mirrored records are unfolded. Local and unpersisted, unlike the
/// section's own fold: opening the receipts is a thing you do once to answer a
/// question, not a way you prefer to read the board.
/// Past this the board scrolls rather than pushing the sections above it off the rail.
/// Every note is on the board and reachable — this is only how much of it is on screen
/// before the wheel takes over. About ten posts at the rail's default width, which is
/// the number asked for: enough to read a conversation, not so many that the rail
/// is nothing but the board.
/// The most the board's scroll box will ever be, on any window: about ten posts at
/// the rail's default width — enough to read a conversation, not so many that the
/// rail is nothing but the board. The rail hands down a smaller cap on a short
/// window (`LoopWorkspaceRail.artifactoryHeightCap`); this is the ceiling on that.
static let maxScrollHeight: CGFloat = 600

@State private var showsRecords = false
Expand Down Expand Up @@ -112,7 +115,7 @@ struct ArtifactorySection: View {
// content's, and `frame(maxHeight:)` under it clamps that. The box is exactly
// as tall as the posts until the cap, and scrolls after — no state, nothing to
// go stale, nothing to fire late.
.frame(maxHeight: Self.maxScrollHeight)
.frame(maxHeight: maxHeight)
.fixedSize(horizontal: false, vertical: true)
// Outside the scroll view on purpose. Inside it the composer was one more row
// in a list that can be taller than the rail — it could open scrolled out of
Expand Down
30 changes: 26 additions & 4 deletions graphcode/Sources/Features/LoopWorkspace/LoopWorkspaceRail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,32 @@ struct LoopWorkspaceRail: View {
graph.edges.filter { $0.to == node.id }.compactMap { graph.nodes[id: $0.from] }
}

/// The most of the rail the board may take before it scrolls, as a share of the
/// rail's own height. The other flexible sections — the summary, the diagram — are
/// greedy and yield; the board hugs its posts with `fixedSize` and does not. On a
/// short window a fixed 600pt cap was enough, with THIS LOOP's 118 and the summary's
/// 120 floor, to overflow the stack and push the foot of the rail off the bottom.
/// A share cannot overflow on its own, and 40% still shows a conversation.
static func artifactoryHeightCap(railHeight: CGFloat) -> CGFloat {
min(ArtifactorySection.maxScrollHeight, max(160, railHeight * 0.4))
}

var body: some View {
// Measured at the outside, where a `GeometryReader` is the container and not a
// guess from within a scroll view — the rail's height is what the board's share
// is a share *of*.
GeometryReader { proxy in
stack(artifactoryCap: Self.artifactoryHeightCap(railHeight: proxy.size.height))
}
.frame(width: width)
.frame(maxHeight: .infinity)
.background(Theme.workspaceRail)
.overlay(alignment: .leading) {
Rectangle().fill(.white.opacity(0.07)).frame(width: 1)
}
}

private func stack(artifactoryCap: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 10) {
// Above `THIS LOOP` rather than below it: what the loop is doing this second
// outranks where it sits in the graph, and a section you have to scroll to is a
Expand Down Expand Up @@ -244,17 +269,14 @@ struct LoopWorkspaceRail: View {
if ArtifactoryPresentation.hasContent(graph: graph, enabled: artifactoryEnabled) {
ArtifactorySection(
graph: graph, seenPostID: seenArtifactoryPostID, isFolded: isArtifactoryFolded,
maxHeight: artifactoryCap,
onToggleFold: onArtifactoryFoldToggled, onPost: onArtifactoryPost)
}
footer
}
.padding(12)
.frame(width: width, alignment: .leading)
.frame(maxHeight: .infinity)
.background(Theme.workspaceRail)
.overlay(alignment: .leading) {
Rectangle().fill(.white.opacity(0.07)).frame(width: 1)
}
}

private func section<Content: View>(
Expand Down
37 changes: 37 additions & 0 deletions graphcode/Tests/ArtifactoryRailShareTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import Testing

@testable import graphcode

/// The board's scroll box is capped by a share of the rail, so a rigid box can never be
/// what pushes the foot of the rail off a short window.
@Suite
struct ArtifactoryRailShareTests {
@Test
func theBoardTakesAShareOfTheRailNotAFixedHeight() {
// A 900pt rail: 40% is 360, well under the 600 ceiling and well over the floor.
#expect(LoopWorkspaceRail.artifactoryHeightCap(railHeight: 900) == 360)
// A tall rail: the share would exceed the ceiling, so the ceiling wins — ten posts
// is enough on any window.
#expect(LoopWorkspaceRail.artifactoryHeightCap(railHeight: 2000) == 600)
#expect(ArtifactorySection.maxScrollHeight == 600)
}

/// A very short rail still shows a couple of posts rather than a sliver; below the
/// floor the rail has bigger problems than this section.
@Test
func aShortRailStillShowsSomething() {
#expect(LoopWorkspaceRail.artifactoryHeightCap(railHeight: 300) == 160)
#expect(LoopWorkspaceRail.artifactoryHeightCap(railHeight: 0) == 160)
}

/// The share leaves room for everything rigid above it: THIS LOOP (118) plus the
/// summary's floor (120) plus chrome, on a rail that fits one 1280×800 window.
@Test
func theShareLeavesRoomForTheRigidSections() {
let rail: CGFloat = 700
let board = LoopWorkspaceRail.artifactoryHeightCap(railHeight: rail)
let rigidAbove: CGFloat = 118 + 120 + 24 + 24 + 12 * 2
#expect(board + rigidAbove < rail)
}
}
Loading