Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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 @@ -10,10 +10,13 @@ protocol TestCaseView: View {
#if BENCHMARK_VIZ
import DefaultBackend

@MainActor
var visualizationSize: (width: Int?, height: Int?) = (nil, nil)

struct VizApp<V: TestCaseView>: App {
var body: some Scene {
WindowGroup("Benchmark visualisation") {
V()
WindowGroup("Benchmark visualization") {
V().frame(width: visualizationSize.width, height: visualizationSize.height)
}
}
}
Expand All @@ -34,36 +37,39 @@ struct Benchmarks {
}

@MainActor
func updateNode<V: View>(_ node: ViewGraphNode<V, DummyBackend>, _ size: SIMD2<Int>) {
_ = node.update(proposedSize: size, environment: environment, dryRun: true)
_ = node.update(proposedSize: size, environment: environment, dryRun: false)
func updateNode<V: View>(_ node: ViewGraphNode<V, DummyBackend>, _ size: ProposedViewSize) {
_ = node.computeLayout(proposedSize: size, environment: environment)
_ = node.commit()
}

#if BENCHMARK_VIZ
var benchmarkVisualizations: [(name: String, main: () -> Never)] = []
var benchmarkVisualizations: [
(name: String, size: ProposedViewSize, main: () -> Never)
] = []
#endif

@MainActor
func benchmarkLayout<V: TestCaseView>(of viewType: V.Type, _ size: SIMD2<Int>, _ label: String) {
func benchmarkLayout<V: TestCaseView>(of viewType: V.Type, _ size: ProposedViewSize, _ label: String) {
#if BENCHMARK_VIZ
benchmarkVisualizations.append((
label,
size,
{
VizApp<V>.main()
exit(0)
}
))
#else
let node = makeNode(V())
benchmark(label) { @MainActor in
let node = makeNode(V())
updateNode(node, size)
}
#endif
}

// Register benchmarks
benchmarkLayout(of: GridView.self, SIMD2(800, 800), "grid")
benchmarkLayout(of: ScrollableMessageListView.self, SIMD2(800, 800), "message list")
benchmarkLayout(of: GridView.self, ProposedViewSize(800, 800), "grid")
benchmarkLayout(of: ScrollableMessageListView.self, ProposedViewSize(800, 800), "message list")

#if BENCHMARK_VIZ
let names = benchmarkVisualizations.map(\.name).joined(separator: " | ")
Expand All @@ -82,6 +88,11 @@ struct Benchmarks {
exit(1)
}

visualizationSize = (
benchmark.size.width.map(Int.init),
benchmark.size.height.map(Int.init)
)

benchmark.main()
#else
await Benchmark.main()
Expand Down
28 changes: 16 additions & 12 deletions Examples/Sources/NotesExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,25 @@ struct ContentView: View {
}
.frame(minWidth: 200)
} detail: {
ScrollView {
VStack(alignment: .center) {
if let selectedNote = selectedNote {
HStack(spacing: 4) {
Text("Title")
TextField("Title", text: selectedNote.title)
}
GeometryReader { proxy in
ScrollView {
VStack(alignment: .center) {
if let selectedNote = selectedNote {
HStack(spacing: 4) {
Text("Title")
TextField("Title", text: selectedNote.title)
}

TextEditor(text: selectedNote.content)
.padding()
.background(textEditorBackground)
.cornerRadius(4)
TextEditor(text: selectedNote.content)
.padding()
.background(textEditorBackground)
.cornerRadius(4)
.frame(maxHeight: .infinity)
}
}
.padding()
.frame(minHeight: Int(proxy.size.height))
}
.padding()
}
}
}
Expand Down
13 changes: 2 additions & 11 deletions Examples/Sources/PathsExample/PathsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,8 @@ struct ArcShape: StyledShape {
}

func size(fitting proposal: SIMD2<Int>) -> ViewSize {
let diameter = max(11, min(proposal.x, proposal.y))
return ViewSize(
size: SIMD2(x: diameter, y: diameter),
idealSize: SIMD2(x: 100, y: 100),
idealWidthForProposedHeight: proposal.y,
idealHeightForProposedWidth: proposal.x,
minimumWidth: 11,
minimumHeight: 11,
maximumWidth: nil,
maximumHeight: nil
)
let diameter = Double(max(11, min(proposal.x, proposal.y)))
return ViewSize(diameter, diameter)
}
}

Expand Down
17 changes: 13 additions & 4 deletions Examples/Sources/WebViewExample/WebViewApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ struct WebViewApp: App {

@State var url = URL(string: "https://stackotter.dev")!

func go(_ url: String) {
guard let url = URL(string: urlInput) else {
return
}

self.url = url
}

var body: some Scene {
WindowGroup("WebViewExample") {
#hotReloadable {
VStack {
HStack {
TextField("URL", text: $urlInput)
Button("Go") {
guard let url = URL(string: urlInput) else {
return // disabled
.onSubmit {
go(urlInput)
}

self.url = url
Button("Go") {
go(urlInput)
}.disabled(URL(string: urlInput) == nil)
}
.padding()
Expand All @@ -36,5 +44,6 @@ struct WebViewApp: App {
}
}
}
.defaultSize(width: 800, height: 800)
}
}
2 changes: 2 additions & 0 deletions Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ struct WindowingApp: App {
}

Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
.resizable()
.aspectRatio(contentMode: .fit)

Divider()

Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ let package = Package(
name: "SwiftCrossUITests",
dependencies: [
"SwiftCrossUI",
"DummyBackend",
.target(name: "AppKitBackend", condition: .when(platforms: [.macOS])),
]
),
Expand Down
32 changes: 10 additions & 22 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public final class AppKitBackend: AppBackend {
// We assume that all scrollers have their controlSize set to `.regular` by default.
// The internet seems to indicate that this is true regardless of any system wide
// preferences etc.
Int(
return Int(
NSScroller.scrollerWidth(
for: .regular,
scrollerStyle: NSScroller.preferredScrollerStyle
Expand Down Expand Up @@ -523,32 +523,17 @@ public final class AppKitBackend: AppBackend {
public func size(
of text: String,
whenDisplayedIn widget: Widget,
proposedFrame: SIMD2<Int>?,
proposedWidth: Int?,
proposedHeight: Int?,
environment: EnvironmentValues
) -> SIMD2<Int> {
if let proposedFrame, proposedFrame.x == 0 {
// We want the text to have the same height as it would have if it were
// one pixel wide so that the layout doesn't suddely jump when the text
// reaches zero width.
let size = size(
of: text,
whenDisplayedIn: widget,
proposedFrame: SIMD2(1, proposedFrame.y),
environment: environment
)
return SIMD2(
0,
size.y
)
}

let proposedSize = NSSize(
width: (proposedFrame?.x).map(CGFloat.init) ?? 0,
height: .greatestFiniteMagnitude
width: proposedWidth.map(Double.init) ?? .greatestFiniteMagnitude,
height: proposedHeight.map(Double.init) ?? .greatestFiniteMagnitude
)
let rect = NSString(string: text).boundingRect(
with: proposedSize,
options: [.usesLineFragmentOrigin],
options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine],
attributes: Self.attributes(forTextIn: environment)
)
return SIMD2(
Expand All @@ -564,6 +549,7 @@ public final class AppKitBackend: AppBackend {
// styles when clicked (yeah that happens...)
field.allowsEditingTextAttributes = true
field.isSelectable = false
field.cell?.truncatesLastVisibleLine = true
return field
}

Expand Down Expand Up @@ -2253,7 +2239,9 @@ public class NSCustomWindow: NSWindow {
}

let contentSize = sender.contentRect(
forFrameRect: NSRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
forFrameRect: NSRect(
x: sender.frame.origin.x, y: sender.frame.origin.y, width: frameSize.width,
height: frameSize.height)
)

resizeHandler(
Expand Down
53 changes: 20 additions & 33 deletions Sources/AppKitBackend/NSViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public protocol NSViewRepresentable: View where Content == Never {
/// for the maximum width/height if the view has no maximum size (and
/// therefore may occupy the entire screen).
func determineViewSize(
for proposal: SIMD2<Int>,
for proposal: ProposedViewSize,
nsView: NSViewType,
context: NSViewRepresentableContext<Coordinator>
) -> ViewSize
Expand All @@ -76,34 +76,16 @@ extension NSViewRepresentable {
}

public func determineViewSize(
for proposal: SIMD2<Int>, nsView: NSViewType,
for proposal: ProposedViewSize,
nsView: NSViewType,
context _: NSViewRepresentableContext<Coordinator>
) -> ViewSize {
let intrinsicSize = nsView.intrinsicContentSize
let sizeThatFits = nsView.fittingSize

let roundedSizeThatFits = SIMD2(
Int(sizeThatFits.width.rounded(.up)),
Int(sizeThatFits.height.rounded(.up)))
let roundedIntrinsicSize = SIMD2(
Int(intrinsicSize.width.rounded(.awayFromZero)),
Int(intrinsicSize.height.rounded(.awayFromZero)))

return ViewSize(
size: SIMD2(
intrinsicSize.width < 0.0 ? proposal.x : roundedSizeThatFits.x,
intrinsicSize.height < 0.0 ? proposal.y : roundedSizeThatFits.y
),
// The 10 here is a somewhat arbitrary constant value so that it's always the same.
// See also `Color` and `Picker`, which use the same constant.
idealSize: SIMD2(
intrinsicSize.width < 0.0 ? 10 : roundedIntrinsicSize.x,
intrinsicSize.height < 0.0 ? 10 : roundedIntrinsicSize.y
),
minimumWidth: max(0, roundedIntrinsicSize.x),
minimumHeight: max(0, roundedIntrinsicSize.x),
maximumWidth: nil,
maximumHeight: nil
intrinsicSize.width < 0 ? (proposal.width ?? 10) : sizeThatFits.width,
intrinsicSize.height < 0 ? (proposal.height ?? 10) : sizeThatFits.height
)
}
}
Expand Down Expand Up @@ -139,15 +121,14 @@ extension View where Self: NSViewRepresentable {
}
}

public func update<Backend: AppBackend>(
public func computeLayout<Backend: AppBackend>(
_ widget: Backend.Widget,
children: any ViewGraphNodeChildren,
proposedSize: SIMD2<Int>,
proposedSize: ProposedViewSize,
environment: EnvironmentValues,
backend: Backend,
dryRun: Bool
) -> ViewUpdateResult {
guard let backend = backend as? AppKitBackend else {
backend: Backend
) -> ViewLayoutResult {
guard backend is AppKitBackend else {
fatalError("NSViewRepresentable updated by \(Backend.self)")
}

Expand All @@ -160,11 +141,17 @@ extension View where Self: NSViewRepresentable {
context: representingWidget.context!
)

if !dryRun {
backend.setSize(of: representingWidget, to: size.size)
}
return ViewLayoutResult.leafView(size: size)
}

return ViewUpdateResult.leafView(size: size)
public func commit<Backend: AppBackend>(
_ widget: Backend.Widget,
children: any ViewGraphNodeChildren,
layout: ViewLayoutResult,
environment: EnvironmentValues,
backend: Backend
) {
backend.setSize(of: widget, to: layout.size.vector)
}
}

Expand Down
Loading
Loading