diff --git a/Sources/Showcase/ImagePlayground.swift b/Sources/Showcase/ImagePlayground.swift index ddbbad9..63227d1 100644 --- a/Sources/Showcase/ImagePlayground.swift +++ b/Sources/Showcase/ImagePlayground.swift @@ -14,273 +14,70 @@ struct ImagePlayground: View { var body: some View { ScrollView { VStack(spacing: 16) { - NavigationLink("Pager") { - ImagePlaygroundPagerView() - } - NavigationLink("Complex Layout (Landscape)") { - ImagePlaygroundComplexLayoutView(imageName: "Cat") - } - NavigationLink("Complex Layout (Portrait)") { - ImagePlaygroundComplexLayoutView(imageName: "CatPortrait") - } - - Text("Asset JPEG Image").font(.title).bold() - HStack { - Spacer() - Image("Cat", bundle: .module, label: Text("Cat JPEG image")) - .resizable() - .aspectRatio(contentMode: .fit) - .border(.yellow, width: 5.0) - .clipShape(RoundedRectangle(cornerSize: CGSize(width: 15, height: 15))) - Spacer() - } - - Text("Asset SVG Image").font(.title).bold() - HStack { - Spacer() - Image("Butterfly", bundle: .module, label: Text("Butterfly SVG image")) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundStyle(.red) - Spacer() - } - - Text("Bundled Image").font(.title).bold() - HStack { - Spacer() - AsyncImage(url: localImageResourceURL) - .border(.blue) - Spacer() - } - - Text("PDF Image").font(.title).bold() HStack { + Text("No URL") Spacer() - Image("skiplogo", bundle: .module, label: Text("skiplogo PDF image")) - .resizable() - .aspectRatio(contentMode: .fit) - Spacer() - } - - #if os(macOS) - #else - Text("Image from Data").font(.title).bold() - HStack { - Spacer() - Image(uiImage: UIImage(data: try! Data(contentsOf: localImageResourceURL!))!) - .border(.blue) - Spacer() - } - #endif - - Text("Symbol Image Weights").font(.title).bold() - HStack { - // This symbol was downloaded from the Google Material Icons catalog and imported into the Module.xcassets: https://fonts.google.com/icons?selected=Material+Symbols+Outlined:passkey:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=passkey&icon.size=24&icon.color=%235f6368&icon.platform=ios - Image("passkey_passkey_symbol", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundStyle(.red) - .fontWeight(.ultraLight) - .frame(width: 80.0, height: 80.0) - Image("passkey_passkey_symbol", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundStyle(.green) - .frame(width: 80.0, height: 80.0) - Image("passkey_passkey_symbol", bundle: .module, label: Text("Passkey")) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundStyle(.blue) - .fontWeight(.black) - .frame(width: 80.0, height: 80.0) - } - - Text("Symbol Image Sizes").font(.title).bold() - HStack { - Image("textformat.size.smaller", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 80.0, height: 80.0) - Image("textformat.size", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 80.0, height: 80.0) - Image("textformat.size.larger", bundle: .module) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 80.0, height: 80.0) - } - - Text("systemName").font(.title).bold() - HStack { - Text(".frame(100, 100)") - Spacer() - Image(systemName: systemNameSample) - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable\n.frame(100, 100)") - Spacer() - Image(systemName: systemNameSample) - .resizable() - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable()\n.scaleToFill\n.frame(100, 100)\n.clipped") - Spacer() - Image(systemName: systemNameSample) - .resizable() - .scaledToFill() - .frame(width: 100, height: 100) - .clipped() - .border(Color.blue) - } - HStack { - Text(".resizable()\n.scaleToFit\n.frame(100, 100)") - Spacer() - Image(systemName: systemNameSample) - .resizable() - .scaledToFit() - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable()\n.aspectRatio(0.33, .fill)\n.frame(100, 100)\n.clipped") - Spacer() - Image(systemName: systemNameSample) - .resizable() - .aspectRatio(0.33, contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - .border(Color.blue) - } - HStack { - Text(".resizable()\n.aspectRatio(0.33, .fit)\n.frame(100, 100)") - Spacer() - Image(systemName: systemNameSample) - .resizable() - .aspectRatio(0.33, contentMode: .fit) - .frame(width: 100, height: 100) + AsyncImage(url: nil) .border(Color.blue) } HStack { - Text(".resizable()\n.aspectRatio(3, .fit)\n.frame(100, 100)\n.foregroundStyle(.red)") + Text("No URL\n.frame(100, 100)") Spacer() - Image(systemName: systemNameSample) - .resizable() - .aspectRatio(3, contentMode: .fit) + AsyncImage(url: nil) .frame(width: 100, height: 100) - .foregroundStyle(.red) .border(Color.blue) } - - Text("AsyncImage").font(.title).bold() HStack { + Text("Just green") Spacer() - AsyncImage(url: remoteImageResourceURL) + Color.green + #if SKIP + .composeModifier { $0.logLayout2("Color") } + #endif + .aspectRatio(3.0/4.0, contentMode: .fit) .border(Color.blue) } + #if SKIP + .composeModifier { $0.logLayout("Color HStack") } + #endif HStack { - Text("scale: 2") + Text("No URL\n.aspectRatio(3/4)") Spacer() - AsyncImage(url: remoteImageResourceURL, scale: 2) + AsyncImage(url: nil) { image in + EmptyView() + } placeholder: { + Color.green + #if SKIP + .composeModifier { $0.logLayout("Placeholder") } + #endif + } + #if SKIP + .composeModifier { $0.logLayout("AsyncImage") } + #endif + .aspectRatio(3.0/4.0, contentMode: .fit) .border(Color.blue) } HStack { - Text(".frame(100, 100)") + Text("No URL\n.aspectRatio(3/4).frame(100)") Spacer() - AsyncImage(url: remoteImageResourceURL) - .frame(width: 100, height: 100) + AsyncImage(url: nil) + .aspectRatio(3.0/4.0, contentMode: .fit) + .frame(width: 100) .border(Color.blue) } HStack { - Text(".frame(100, 100)\nclipped") + Text("Failed URL\n.aspectRatio(3/4)") Spacer() - AsyncImage(url: remoteImageResourceURL) - .frame(width: 100, height: 100) - .clipped() + AsyncImage(url: URL(string: "file:///does_not_exist.png")!) + .aspectRatio(3.0/4.0, contentMode: .fit) .border(Color.blue) } HStack { - Text(".resizable()\n.frame(100, 100)") - Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable()\n.scaleToFill\n.frame(100, 100)\n.clipped") + Text("Failed URL\n.aspectRatio(3/4).frame(100)") Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .scaledToFill() - .frame(width: 100, height: 100) - .clipped() - .border(Color.blue) - } - HStack { - Text(".resizable()\n.scaleToFit\n.frame(100, 100)") - Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .scaledToFit() - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable()\n.aspectRatio(0.33, .fill)\n.frame(100, 100)\n.clipped") - Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .aspectRatio(0.33, contentMode: .fill) - .frame(width: 100, height: 100) - .clipped() - .border(Color.blue) - } - HStack { - Text(".resizable()\n.aspectRatio(0.33, .fit)\n.frame(100, 100)") - Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .aspectRatio(0.33, contentMode: .fit) - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text(".resizable()\n.aspectRatio(3, .fit)\n.frame(100, 100)") - Spacer() - AsyncImage(url: remoteImageResourceURL) { image in - image.resizable() - } placeholder: { - } - .aspectRatio(3, contentMode: .fit) - .frame(width: 100, height: 100) - .border(Color.blue) - } - HStack { - Text("No URL") - Spacer() - AsyncImage(url: nil) - .border(Color.blue) - } - HStack { - Text("No URL\n.frame(100, 100)") - Spacer() - AsyncImage(url: nil) - .frame(width: 100, height: 100) + AsyncImage(url: URL(string: "file:///does_not_exist.png")!) + .aspectRatio(3.0/4.0, contentMode: .fit) + .frame(width: 100) .border(Color.blue) } HStack { @@ -388,3 +185,20 @@ private struct ImagePlaygroundComplexLayoutView: View { } } } + +#if SKIP +import androidx.compose.ui.Modifier + +extension Modifier { + /// Log layout constraints for debugging purposes. + /// + /// - Parameter tag: The log tag to use (default: "LogLayout"). + /// - Returns: A modifier that logs layout constraints and bounds. + /// + public func logLayout2(tag: String = "LogLayout") -> Modifier { + return self.logLayoutModifier2(tag: tag) + } +} + + +#endif \ No newline at end of file diff --git a/Sources/Showcase/Resources/Localizable.xcstrings b/Sources/Showcase/Resources/Localizable.xcstrings index 5267d18..7757958 100644 --- a/Sources/Showcase/Resources/Localizable.xcstrings +++ b/Sources/Showcase/Resources/Localizable.xcstrings @@ -162,12 +162,6 @@ }, ".frame.animation" : { - }, - ".frame(100, 100)" : { - - }, - ".frame(100, 100)\nclipped" : { - }, ".grayscale(0.25)" : { @@ -319,30 +313,6 @@ }, ".repeatForever()" : { - }, - ".resizable\n.frame(100, 100)" : { - - }, - ".resizable()\n.aspectRatio(0.33, .fill)\n.frame(100, 100)\n.clipped" : { - - }, - ".resizable()\n.aspectRatio(0.33, .fit)\n.frame(100, 100)" : { - - }, - ".resizable()\n.aspectRatio(3, .fit)\n.frame(100, 100)" : { - - }, - ".resizable()\n.aspectRatio(3, .fit)\n.frame(100, 100)\n.foregroundStyle(.red)" : { - - }, - ".resizable()\n.frame(100, 100)" : { - - }, - ".resizable()\n.scaleToFill\n.frame(100, 100)\n.clipped" : { - - }, - ".resizable()\n.scaleToFit\n.frame(100, 100)" : { - }, ".rotation3DEffects" : { @@ -818,12 +788,6 @@ } } } - }, - "Asset JPEG Image" : { - - }, - "Asset SVG Image" : { - }, "Asymmetric: opacity+slide" : { @@ -923,12 +887,6 @@ }, "Brown" : { - }, - "Bundled Image" : { - - }, - "Butterfly SVG image" : { - }, "Button" : { @@ -998,12 +956,6 @@ }, "Complex content button" : { - }, - "Complex Layout (Landscape)" : { - - }, - "Complex Layout (Portrait)" : { - }, "Compose" : { @@ -1249,6 +1201,12 @@ }, "F2" : { + }, + "Failed URL\n.aspectRatio(3/4)" : { + + }, + "Failed URL\n.aspectRatio(3/4).frame(100)" : { + }, "Favorites" : { @@ -1372,12 +1330,6 @@ }, "Footer 3" : { - }, - "Footer line 1" : { - - }, - "Footer line 2" : { - }, "ForEach index row: %lld" : { @@ -1478,12 +1430,6 @@ }, "Header" : { - }, - "Header line 1" : { - - }, - "Header line 2" : { - }, "height: 50" : { @@ -1570,9 +1516,6 @@ }, "Image" : { - }, - "Image from Data" : { - }, "in: 0...2 step 0.5" : { @@ -1648,6 +1591,9 @@ }, "JavaScript" : { + }, + "Just green" : { + }, "Key" : { @@ -1956,6 +1902,12 @@ }, "No URL" : { + }, + "No URL\n.aspectRatio(3/4)" : { + + }, + "No URL\n.aspectRatio(3/4).frame(100)" : { + }, "No URL\n.frame(100, 100)" : { @@ -2044,9 +1996,6 @@ }, "Page 2" : { - }, - "Pager" : { - }, "Paging" : { @@ -2056,9 +2005,6 @@ }, "Passing a string var does not format as markdown:" : { - }, - "Passkey" : { - }, "Password" : { @@ -2117,9 +2063,6 @@ }, "Patterns:" : { - }, - "PDF Image" : { - }, "Pick Document" : { @@ -2330,9 +2273,6 @@ }, "Row 2.1" : { - }, - "Row 3.1" : { - }, "Row 3a" : { @@ -2360,9 +2300,6 @@ }, "scale, rotate, offset, stroke" : { - }, - "scale: 2" : { - }, "scale(x: 0.5, y: 1.2)" : { @@ -2757,9 +2694,6 @@ } } } - }, - "skiplogo PDF image" : { - }, "Slider" : { @@ -2889,12 +2823,6 @@ }, "Symbol" : { - }, - "Symbol Image Sizes" : { - - }, - "Symbol Image Weights" : { - }, "System" : { @@ -2904,9 +2832,6 @@ }, "System Settings" : { - }, - "systemName" : { - }, "Table" : { diff --git a/Sources/Showcase/Skip/LogLayout2.kt b/Sources/Showcase/Skip/LogLayout2.kt new file mode 100644 index 0000000..3b9dc26 --- /dev/null +++ b/Sources/Showcase/Skip/LogLayout2.kt @@ -0,0 +1,134 @@ +// Copyright 2025 Skip +// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception +package skip.ui + +import android.util.Log +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.node.GlobalPositionAwareModifierNode +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Constraints + +/** + * Creates a Modifier that logs layout constraints and bounds for debugging. + * + * This modifier is a pass-through that doesn't affect layout behavior. It uses a stable + * modifier element (with proper equals/hashCode based on the tag) to prevent remeasurement + * loops that can occur when modifiers are recreated on every recomposition. + */ +public fun Modifier.logLayoutModifier2(tag: String): Modifier { + return this then LogLayoutElement2(tag) +} + +private class LogLayoutElement2(private val tag: String) : ModifierNodeElement() { + override fun create(): LogLayoutModifierNode2 { + return LogLayoutModifierNode2(tag) + } + + override fun update(node: LogLayoutModifierNode2) { + node.tag = tag + } + + override fun InspectorInfo.inspectableProperties() { + name = "logLayout" + properties["tag"] = tag + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LogLayoutElement2) return false + return tag == other.tag + } + + override fun hashCode(): Int { + return tag.hashCode() + } +} + +private class LogLayoutModifierNode2( + var tag: String +) : LayoutModifierNode, GlobalPositionAwareModifierNode, Modifier.Node() { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + Log.d( + tag, + "Constraints: minWidth=${constraints.minWidth}, maxWidth=${constraints.maxWidth}, " + + "minHeight=${constraints.minHeight}, maxHeight=${constraints.maxHeight}" + ) + val placeable = measurable.measure(constraints) + Log.d( + tag, + "Measured: width=${placeable.width}, height=${placeable.height}" + ) + return layout(width = placeable.width, height = placeable.height) { + placeable.placeRelative(x = 0, y = 0) + } + } + + override fun IntrinsicMeasureScope.maxIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ): Int { + val result = measurable.maxIntrinsicWidth(height) + Log.d( + tag, + "maxIntrinsicWidth: height=$height, result=$result" + ) + return result + } + + override fun IntrinsicMeasureScope.maxIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ): Int { + val result = measurable.maxIntrinsicHeight(width) + Log.d( + tag, + "maxIntrinsicHeight: width=$width, result=$result" + ) + return result + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ): Int { + val result = measurable.minIntrinsicWidth(height) + Log.d( + tag, + "minIntrinsicWidth: height=$height, result=$result" + ) + return result + } + + override fun IntrinsicMeasureScope.minIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ): Int { + val result = measurable.minIntrinsicHeight(width) + Log.d( + tag, + "minIntrinsicHeight: width=$width, result=$result" + ) + return result + } + + override fun onGloballyPositioned(coordinates: LayoutCoordinates) { + val bounds = coordinates.boundsInWindow() + Log.d( + tag, + "Bounds: (top=${bounds.top}, left=${bounds.left}, bottom=${bounds.bottom}, " + + "right=${bounds.right}, width=${bounds.width}, height=${bounds.height})" + ) + } +}