Skip to content

Commit 8c87033

Browse files
committed
Merge branch 'main' of https://github.com/gonzalezreal/textual into syntax-extensions
2 parents 034f5b0 + d34d16e commit 8c87033

11 files changed

Lines changed: 214 additions & 116 deletions

File tree

Examples/TextualDemo/TextualDemo/TableDemo.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import SwiftUI
22
import Textual
33

44
struct TableDemo: View {
5+
@State private var relativeWidth: CGFloat = 2.1
6+
57
private let content = """
68
Sometimes it helps to step back and *observe the situation calmly*, especially
79
when the codebase feels larger than it should. You take a breath, open the
@@ -32,6 +34,23 @@ struct TableDemo: View {
3234
for future you, and walk away while things are still calm. The code will still be
3335
here tomorrow, probably waiting patiently :awwwblob:.
3436
"""
37+
private let overflowContent = """
38+
When the status board grows beyond the comfort of the sidebar, it’s time for a wider view.
39+
40+
| Feature Area | Owner | Status | Notes |
41+
|------------------|------------------|--------------|-----------------------------------------------------------------------|
42+
| Attachments | Casey | In progress | Needs caching strategy; large emoji sets are still slow to resolve. |
43+
| Selection | Drew | Investigating| Selection handles are jittery with nested lists and inline links. |
44+
| Rendering | Jae | Stable | Layout passes are predictable, but long cells should wrap cleanly. |
45+
46+
After a few iterations, priorities shift and a more detailed breakdown appears.
47+
48+
| Milestone | Target Date | Dependency | Notes |
49+
|----------------------|-------------|-------------------|--------------------------------------------------------|
50+
| Rendering polish | Sep 18 | Table overlays | Needs scrollable headers without losing alignment. |
51+
| Selection fixes | Sep 25 | Text layout | Requires stable geometry on fast resize changes. |
52+
| Attachment pipeline | Oct 02 | Caching strategy | Large emoji sets should avoid repeated decode work. |
53+
"""
3554

3655
var body: some View {
3756
Form {
@@ -45,6 +64,20 @@ struct TableDemo: View {
4564
Text("Default Style")
4665
Text("Text Selection Enabled")
4766
}
67+
Section {
68+
HStack {
69+
Text("Relative Width")
70+
Slider(value: $relativeWidth, in: 1...3)
71+
}
72+
StructuredText(
73+
markdown: overflowContent,
74+
patternOptions: .init(emoji: .mastoEmoji)
75+
)
76+
} header: {
77+
Text("Overflow Style")
78+
Text("Horizontal Scroll")
79+
}
80+
.textual.tableStyle(.overflow(relativeWidth: relativeWidth))
4881
Section("GitHub Style") {
4982
StructuredText(
5083
markdown: content,
Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import SwiftUI
22

3-
// NB: Enables environment resolution in `TableStyle` and its background / overlay hooks.
3+
// NB: Enables environment resolution in `TableStyle`
44

55
extension StructuredText {
66
struct ResolvedTableStyle<S: TableStyle>: View {
@@ -16,46 +16,10 @@ extension StructuredText {
1616
style.makeBody(configuration: configuration)
1717
}
1818
}
19-
20-
struct ResolvedTableBackground<S: TableStyle>: View {
21-
private let style: S
22-
private let layout: TableLayout
23-
24-
init(_ style: S, layout: TableLayout) {
25-
self.style = style
26-
self.layout = layout
27-
}
28-
29-
var body: S.Background {
30-
style.makeBackground(layout: layout)
31-
}
32-
}
33-
34-
struct ResolvedTableOverlay<S: TableStyle>: View {
35-
private let style: S
36-
private let layout: TableLayout
37-
38-
init(_ style: S, layout: TableLayout) {
39-
self.style = style
40-
self.layout = layout
41-
}
42-
43-
var body: S.Overlay {
44-
style.makeOverlay(layout: layout)
45-
}
46-
}
4719
}
4820

4921
extension StructuredText.TableStyle {
5022
@MainActor func resolve(configuration: Configuration) -> some View {
5123
StructuredText.ResolvedTableStyle(self, configuration: configuration)
5224
}
53-
54-
@MainActor func resolveBackground(layout: StructuredText.TableLayout) -> some View {
55-
StructuredText.ResolvedTableBackground(self, layout: layout)
56-
}
57-
58-
@MainActor func resolveOverlay(layout: StructuredText.TableLayout) -> some View {
59-
StructuredText.ResolvedTableOverlay(self, layout: layout)
60-
}
6125
}

Sources/Textual/Internal/StructuredText/Table.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,6 @@ extension StructuredText {
3636
.onPreferenceChange(TableCell.SpacingKey.self) { @MainActor in
3737
spacing = $0
3838
}
39-
.backgroundPreferenceValue(TableCell.BoundsKey.self) { values in
40-
GeometryReader { geometry in
41-
let resolvedBackground = tableStyle.resolveBackground(
42-
layout: .init(values, geometry: geometry)
43-
)
44-
AnyView(resolvedBackground)
45-
}
46-
}
47-
.overlayPreferenceValue(TableCell.BoundsKey.self) { values in
48-
GeometryReader { geometry in
49-
let resolvedOverlay = tableStyle.resolveOverlay(
50-
layout: .init(values, geometry: geometry)
51-
)
52-
AnyView(resolvedOverlay)
53-
}
54-
}
5539

5640
AnyView(resolvedStyle)
5741
}

Sources/Textual/StructuredText/Style/Default/DefaultTableStyle.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@ extension StructuredText {
1212
configuration.label
1313
.textual.tableCellSpacing(horizontal: Self.borderWidth, vertical: Self.borderWidth)
1414
.textual.blockSpacing(.fontScaled(top: 1.6, bottom: 1.6))
15-
.padding(Self.borderWidth)
16-
}
17-
18-
public func makeOverlay(layout: StructuredText.TableLayout) -> some View {
19-
Canvas { context, _ in
20-
for divider in layout.dividers() {
21-
context.fill(
22-
Path(divider),
23-
with: .style(DynamicColor.grayTertiary)
24-
)
15+
.textual.tableOverlay { layout in
16+
Canvas { context, _ in
17+
for divider in layout.dividers() {
18+
context.fill(
19+
Path(divider),
20+
with: .style(DynamicColor.grayTertiary)
21+
)
22+
}
23+
}
2524
}
26-
}
25+
.padding(Self.borderWidth)
2726
}
2827
}
2928
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import SwiftUI
2+
3+
extension StructuredText {
4+
/// A table style that enables horizontal scrolling with a relative max width ratio.
5+
///
6+
/// Use ``TextualNamespace/overflowMode(_:)`` to switch between scrolling and wrapping.
7+
public struct OverflowTableStyle: TableStyle {
8+
private static let borderWidth: CGFloat = 1
9+
10+
private let relativeWidth: CGFloat
11+
12+
/// Creates an overflow table style.
13+
///
14+
/// - Parameter relativeWidth: The maximum width ratio relative to the scroll container width. Defaults to `1.5`.
15+
public init(relativeWidth: CGFloat = 1.5) {
16+
self.relativeWidth = relativeWidth
17+
}
18+
19+
public func makeBody(configuration: Configuration) -> some View {
20+
Overflow { state in
21+
let maxWidth = state.containerWidth.map {
22+
$0 * relativeWidth
23+
}
24+
configuration.label
25+
.fixedSize(horizontal: false, vertical: true)
26+
.frame(maxWidth: maxWidth, alignment: .leading)
27+
.textual.tableOverlay { layout in
28+
Canvas { context, _ in
29+
for divider in layout.dividers() {
30+
context.fill(
31+
Path(divider),
32+
with: .style(DynamicColor.grayTertiary)
33+
)
34+
}
35+
}
36+
}
37+
.padding(Self.borderWidth)
38+
}
39+
.textual.tableCellSpacing(horizontal: Self.borderWidth, vertical: Self.borderWidth)
40+
.textual.blockSpacing(.fontScaled(top: 1.6, bottom: 1.6))
41+
}
42+
}
43+
}
44+
45+
extension StructuredText.TableStyle where Self == StructuredText.OverflowTableStyle {
46+
/// A table style that enables horizontal scrolling with a relative max width.
47+
public static var overflow: Self {
48+
.init()
49+
}
50+
51+
/// A table style that enables horizontal scrolling with a relative max width.
52+
///
53+
/// - Parameter relativeWidth: The maximum width ratio relative to the scroll container width.
54+
public static func overflow(relativeWidth: CGFloat) -> Self {
55+
.init(relativeWidth: relativeWidth)
56+
}
57+
}
58+
59+
@available(tvOS, unavailable)
60+
@available(watchOS, unavailable)
61+
#Preview {
62+
StructuredText(
63+
markdown: """
64+
The sky above the port was the color of television, tuned to a dead channel.
65+
66+
Sloth speed | Description | Notes
67+
------------ | ------------------------------------- | ---------------------------------------------
68+
`slow` | Moves slightly faster than a snail | Good for sightseeing along the waterfront.
69+
`medium` | Moves at an average speed | Balanced choice for day-to-day commuting.
70+
`fast` | Moves faster than a hare | Best for urgent deliveries across town.
71+
`supersonic` | Moves faster than the speed of sound | Only for the bravest sloths.
72+
73+
It was a bright cold day in April, and the clocks were striking thirteen.
74+
"""
75+
)
76+
.padding()
77+
.textual.tableStyle(.overflow)
78+
}

Sources/Textual/StructuredText/Style/GitHub/GitHubTableStyle.swift

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,28 @@ extension StructuredText {
1212
configuration.label
1313
.textual.tableCellSpacing(horizontal: Self.borderWidth, vertical: Self.borderWidth)
1414
.textual.blockSpacing(.init(top: 0, bottom: 16))
15-
.padding(Self.borderWidth)
16-
.border(DynamicColor.gitHubBorder, width: Self.borderWidth)
17-
}
18-
19-
public func makeBackground(layout: StructuredText.TableLayout) -> some View {
20-
Canvas { context, _ in
21-
for bounds in layout.evenRowBounds {
22-
context.fill(
23-
Path(bounds.integral),
24-
with: .style(DynamicColor.gitHubSecondaryBackground)
25-
)
15+
.textual.tableBackground { layout in
16+
Canvas { context, _ in
17+
for bounds in layout.evenRowBounds {
18+
context.fill(
19+
Path(bounds.integral),
20+
with: .style(DynamicColor.gitHubSecondaryBackground)
21+
)
22+
}
23+
}
2624
}
27-
}
28-
}
29-
30-
public func makeOverlay(layout: StructuredText.TableLayout) -> some View {
31-
Canvas { context, _ in
32-
for divider in layout.dividers() {
33-
context.fill(
34-
Path(divider),
35-
with: .style(DynamicColor.gitHubBorder)
36-
)
25+
.textual.tableOverlay { layout in
26+
Canvas { context, _ in
27+
for divider in layout.dividers() {
28+
context.fill(
29+
Path(divider),
30+
with: .style(DynamicColor.gitHubBorder)
31+
)
32+
}
33+
}
3734
}
38-
}
35+
.padding(Self.borderWidth)
36+
.border(DynamicColor.gitHubBorder, width: Self.borderWidth)
3937
}
4038
}
4139
}

Sources/Textual/StructuredText/Style/Overflow.swift

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ public enum OverflowMode: Hashable {
88
case scroll
99
}
1010

11+
/// Describes the current overflow behavior and available layout metrics.
12+
public enum OverflowState: Hashable {
13+
/// Wraps content to fit the available width.
14+
case wrap
15+
/// Scrolls horizontally. The container width is provided when available.
16+
case scroll(containerWidth: CGFloat?)
17+
18+
/// The scroll container width when available; otherwise `nil`.
19+
public var containerWidth: CGFloat? {
20+
guard case .scroll(let containerWidth) = self else {
21+
return nil
22+
}
23+
return containerWidth
24+
}
25+
}
26+
1127
/// A container that adapts to the current ``OverflowMode``.
1228
///
1329
/// `Overflow` handles content that overflows horizontally. It can switch
@@ -20,28 +36,48 @@ public enum OverflowMode: Hashable {
2036
/// Using a horizontal `ScrollView` directly will interfere with text selection gestures.
2137
public struct Overflow<Content: View>: View {
2238
@Environment(\.overflowMode) private var mode
39+
@State private var containerWidth: CGFloat?
40+
@State private var contentHeight: CGFloat?
2341

24-
private let content: () -> Content
42+
private let content: (OverflowState) -> Content
2543

2644
/// Creates an overflow container.
2745
public init(@ViewBuilder content: @escaping () -> Content) {
46+
self.init { _ in
47+
content()
48+
}
49+
}
50+
51+
/// Creates an overflow container that exposes the current overflow state.
52+
public init(@ViewBuilder content: @escaping (_ state: OverflowState) -> Content) {
2853
self.content = content
2954
}
3055

3156
public var body: some View {
3257
switch mode {
3358
case .wrap:
34-
content()
59+
content(.wrap)
3560
.frame(maxWidth: .infinity, alignment: .leading)
3661

3762
case .scroll:
3863
ScrollView(.horizontal) {
39-
content()
40-
// Make text selection local in scrollable regions
41-
.modifier(TextSelectionInteraction())
42-
.transformPreference(Text.LayoutKey.self) { value in
43-
value = []
44-
}
64+
ZStack {
65+
// Update the scroll view height when the content height changes
66+
Color.clear
67+
.frame(minHeight: contentHeight)
68+
content(.scroll(containerWidth: containerWidth))
69+
.onGeometryChange(for: CGFloat.self, of: \.size.height) {
70+
contentHeight = $0
71+
}
72+
// Make text selection local in scrollable regions
73+
.modifier(TextSelectionInteraction())
74+
.transformPreference(Text.LayoutKey.self) { value in
75+
value = []
76+
}
77+
}
78+
}
79+
.onScrollGeometryChange(for: CGFloat.self, of: \.containerSize.width) {
80+
containerWidth = $1
4581
}
4682
// Propagate gesture exclusion area
4783
.background(

0 commit comments

Comments
 (0)