ive lost track. but this needs to happen. because ive lost track.
|
|
@ -1,12 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg viewBox="0 0 489.61898 491.035" version="1.1" id="svg29" width="489.61899" height="491.035" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" xmlns:bx="https://boxy-svg.com">
|
<svg viewBox="28.548 28.642 432.523 433.752" version="1.1" id="svg29" width="489.61899" height="491.035" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" xmlns:bx="https://boxy-svg.com">
|
||||||
<defs id="defs26">
|
<defs id="defs26">
|
||||||
<path id="text-path-1" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
<path id="text-path-1" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
||||||
<path id="path-2" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
<path id="path-2" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
||||||
<path id="path-3" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
<path id="path-3" d="m 77.219,406.062 c 77.897,-187.809 190.069,-151.33 256.629,-99.786 18.407,14.254 76.718,135.623 76.524,160.854"/>
|
||||||
<filter id="drop-shadow-filter-0" bx:preset="drop-shadow 1 0 2 20 1 #fff" color-interpolation-filters="sRGB" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="drop-shadow-filter-0" bx:preset="drop-shadow 1 0 2 5 1 #fff" color-interpolation-filters="sRGB" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<title>Drop shadow</title>
|
<title>Drop shadow</title>
|
||||||
<feGaussianBlur in="SourceAlpha" stdDeviation="20"/>
|
<feGaussianBlur in="SourceAlpha" stdDeviation="5"/>
|
||||||
<feOffset dx="0" dy="2"/>
|
<feOffset dx="0" dy="2"/>
|
||||||
<feComponentTransfer result="offsetblur">
|
<feComponentTransfer result="offsetblur">
|
||||||
<feFuncA id="spread-ctrl" type="linear" slope="2"/>
|
<feFuncA id="spread-ctrl" type="linear" slope="2"/>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
||||||
<feOffset dx="-3" dy="10"/>
|
<feOffset dx="-3" dy="10"/>
|
||||||
<feComponentTransfer result="offsetblur">
|
<feComponentTransfer result="offsetblur">
|
||||||
<feFuncA id="spread-ctrl" type="linear" slope="1.74"/>
|
<feFuncA id="spread-ctrl-2" type="linear" slope="1.74"/>
|
||||||
</feComponentTransfer>
|
</feComponentTransfer>
|
||||||
<feFlood flood-color="#fff"/>
|
<feFlood flood-color="#fff"/>
|
||||||
<feComposite in2="offsetblur" operator="in"/>
|
<feComposite in2="offsetblur" operator="in"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -7,16 +7,28 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
13355EBDA35C1D1A4C56C4B6 /* ViewportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D4361480B1086E42F1C44D /* ViewportController.swift */; };
|
||||||
|
451B55F9A56914484DE9F387 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CC2AF06102C6AF52E50D50 /* MenuBar.swift */; };
|
||||||
52FAB9972C318EE0F234F494 /* IcedViewportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */; };
|
52FAB9972C318EE0F234F494 /* IcedViewportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */; };
|
||||||
951153057A1C3373C77657DF /* AcordApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E391F925E2727D13659EDA02 /* AcordApp.swift */; };
|
951153057A1C3373C77657DF /* AcordApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E391F925E2727D13659EDA02 /* AcordApp.swift */; };
|
||||||
|
A6236C78901D9A1CDF17D665 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22A61FD61FB3606DC153A210 /* Assets.xcassets */; };
|
||||||
|
C138920C883B9BF2BF57BCFA /* PermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB74A74224F1D3571524FA8 /* PermissionsManager.swift */; };
|
||||||
F0306F25CA56D707AE2E27A0 /* IcedViewportRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */; };
|
F0306F25CA56D707AE2E27A0 /* IcedViewportRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */; };
|
||||||
|
F10B810345E36162B23064AF /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F50B6D3A3481F01ECC66CDBD /* DocumentPicker.swift */; };
|
||||||
|
F6003A9958CB3CA65BAFE23F /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF09AEF6E0546502D3FB36BE /* Debug.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcedViewportRepresentable.swift; sourceTree = "<group>"; };
|
19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcedViewportRepresentable.swift; sourceTree = "<group>"; };
|
||||||
|
22A61FD61FB3606DC153A210 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcedViewportView.swift; sourceTree = "<group>"; };
|
4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcedViewportView.swift; sourceTree = "<group>"; };
|
||||||
|
5FB74A74224F1D3571524FA8 /* PermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsManager.swift; sourceTree = "<group>"; };
|
||||||
|
A1D4361480B1086E42F1C44D /* ViewportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewportController.swift; sourceTree = "<group>"; };
|
||||||
C3417095D399AB8B216B5139 /* Acord.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Acord.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
C3417095D399AB8B216B5139 /* Acord.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Acord.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
CF09AEF6E0546502D3FB36BE /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = "<group>"; };
|
||||||
|
D1CC2AF06102C6AF52E50D50 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = "<group>"; };
|
||||||
E391F925E2727D13659EDA02 /* AcordApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcordApp.swift; sourceTree = "<group>"; };
|
E391F925E2727D13659EDA02 /* AcordApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcordApp.swift; sourceTree = "<group>"; };
|
||||||
|
F50B6D3A3481F01ECC66CDBD /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
|
@ -32,6 +44,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FD6FF6B76E15E1EA3D0E676B /* src */,
|
FD6FF6B76E15E1EA3D0E676B /* src */,
|
||||||
|
22A61FD61FB3606DC153A210 /* Assets.xcassets */,
|
||||||
76C61BCA6D25861221CA6340 /* Products */,
|
76C61BCA6D25861221CA6340 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -40,8 +53,13 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E391F925E2727D13659EDA02 /* AcordApp.swift */,
|
E391F925E2727D13659EDA02 /* AcordApp.swift */,
|
||||||
|
CF09AEF6E0546502D3FB36BE /* Debug.swift */,
|
||||||
|
F50B6D3A3481F01ECC66CDBD /* DocumentPicker.swift */,
|
||||||
19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */,
|
19D7F3B08824BF5E9E468EF2 /* IcedViewportRepresentable.swift */,
|
||||||
4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */,
|
4AF21EB9858D4F182A323DAA /* IcedViewportView.swift */,
|
||||||
|
D1CC2AF06102C6AF52E50D50 /* MenuBar.swift */,
|
||||||
|
5FB74A74224F1D3571524FA8 /* PermissionsManager.swift */,
|
||||||
|
A1D4361480B1086E42F1C44D /* ViewportController.swift */,
|
||||||
);
|
);
|
||||||
path = src;
|
path = src;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -54,6 +72,7 @@
|
||||||
buildConfigurationList = 3E9D564CF91C62A230D65F8C /* Build configuration list for PBXNativeTarget "Acord" */;
|
buildConfigurationList = 3E9D564CF91C62A230D65F8C /* Build configuration list for PBXNativeTarget "Acord" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
BEED27C37617469B917539A1 /* Sources */,
|
BEED27C37617469B917539A1 /* Sources */,
|
||||||
|
5C3B8F8816A1704304EF6942 /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
|
@ -100,14 +119,30 @@
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
5C3B8F8816A1704304EF6942 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A6236C78901D9A1CDF17D665 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
BEED27C37617469B917539A1 /* Sources */ = {
|
BEED27C37617469B917539A1 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
951153057A1C3373C77657DF /* AcordApp.swift in Sources */,
|
951153057A1C3373C77657DF /* AcordApp.swift in Sources */,
|
||||||
|
F6003A9958CB3CA65BAFE23F /* Debug.swift in Sources */,
|
||||||
|
F10B810345E36162B23064AF /* DocumentPicker.swift in Sources */,
|
||||||
F0306F25CA56D707AE2E27A0 /* IcedViewportRepresentable.swift in Sources */,
|
F0306F25CA56D707AE2E27A0 /* IcedViewportRepresentable.swift in Sources */,
|
||||||
52FAB9972C318EE0F234F494 /* IcedViewportView.swift in Sources */,
|
52FAB9972C318EE0F234F494 /* IcedViewportView.swift in Sources */,
|
||||||
|
451B55F9A56914484DE9F387 /* MenuBar.swift in Sources */,
|
||||||
|
C138920C883B9BF2BF57BCFA /* PermissionsManager.swift in Sources */,
|
||||||
|
13355EBDA35C1D1A4C56C4B6 /* ViewportController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -187,9 +222,8 @@
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Info.plist;
|
INFOPLIST_FILE = Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LIBRARY_SEARCH_PATHS = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "/tmp/acord/target/aarch64-apple-ios/release";
|
||||||
"$(PROJECT_DIR)/../target/$(SWIFT_PLATFORM_TARGET_PREFIX)/release",
|
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "/tmp/acord/target/aarch64-apple-ios-sim/release";
|
||||||
);
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
|
@ -258,9 +292,8 @@
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Info.plist;
|
INFOPLIST_FILE = Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LIBRARY_SEARCH_PATHS = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "/tmp/acord/target/aarch64-apple-ios/release";
|
||||||
"$(PROJECT_DIR)/../target/$(SWIFT_PLATFORM_TARGET_PREFIX)/release",
|
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "/tmp/acord/target/aarch64-apple-ios-sim/release";
|
||||||
);
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
|
|
|
||||||
BIN
ios/Acord.xcodeproj/project.xcworkspace/xcuserdata/pszsh.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>SchemeUserState</key>
|
||||||
|
<dict>
|
||||||
|
<key>Acord.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "iphone", "size" : "20x20", "scale" : "2x", "filename" : "Icon-20@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "20x20", "scale" : "3x", "filename" : "Icon-20@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "29x29", "scale" : "2x", "filename" : "Icon-29@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "29x29", "scale" : "3x", "filename" : "Icon-29@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "40x40", "scale" : "2x", "filename" : "Icon-40@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "40x40", "scale" : "3x", "filename" : "Icon-40@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "60x60", "scale" : "2x", "filename" : "Icon-60@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "60x60", "scale" : "3x", "filename" : "Icon-60@3x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "20x20", "scale" : "1x", "filename" : "Icon-20.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "20x20", "scale" : "2x", "filename" : "Icon-20@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "29x29", "scale" : "1x", "filename" : "Icon-29.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "29x29", "scale" : "2x", "filename" : "Icon-29@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "40x40", "scale" : "1x", "filename" : "Icon-40.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "40x40", "scale" : "2x", "filename" : "Icon-40@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "76x76", "scale" : "1x", "filename" : "Icon-76.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "76x76", "scale" : "2x", "filename" : "Icon-76@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "83.5x83.5","scale" : "2x","filename" : "Icon-83.5@2x.png" },
|
||||||
|
{ "idiom" : "ios-marketing","size" : "1024x1024","scale" : "1x","filename" : "Icon-1024.png" }
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,5 +60,68 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIFileSharingEnabled</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UISupportsDocumentBrowser</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Markdown</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Default</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>net.daringfireball.markdown</string>
|
||||||
|
<string>public.plain-text</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>md</string>
|
||||||
|
<string>markdown</string>
|
||||||
|
<string>mdown</string>
|
||||||
|
<string>txt</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Acord uses your photo library to embed images into notes.</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>Acord saves rendered notes and exported PDFs to your photo library.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Acord uses the camera to attach photos directly to notes.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Acord uses the microphone to attach voice notes.</string>
|
||||||
|
<key>NSDocumentsFolderUsageDescription</key>
|
||||||
|
<string>Acord reads and writes notes from your chosen documents folder.</string>
|
||||||
|
<key>NSDownloadsFolderUsageDescription</key>
|
||||||
|
<string>Acord opens and saves notes in your Downloads folder.</string>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>net.daringfireball.markdown</string>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Markdown</string>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.plain-text</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>md</string>
|
||||||
|
<string>markdown</string>
|
||||||
|
<string>mdown</string>
|
||||||
|
</array>
|
||||||
|
<key>public.mime-type</key>
|
||||||
|
<array>
|
||||||
|
<string>text/markdown</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ settings:
|
||||||
TARGETED_DEVICE_FAMILY: "2,1"
|
TARGETED_DEVICE_FAMILY: "2,1"
|
||||||
IPHONEOS_DEPLOYMENT_TARGET: "17.0"
|
IPHONEOS_DEPLOYMENT_TARGET: "17.0"
|
||||||
SWIFT_OBJC_BRIDGING_HEADER: ../viewport/include/acord.h
|
SWIFT_OBJC_BRIDGING_HEADER: ../viewport/include/acord.h
|
||||||
LIBRARY_SEARCH_PATHS:
|
# Cargo writes to /tmp/acord/target by default (see scripts/_build-dirs.sh).
|
||||||
- $(PROJECT_DIR)/../target/$(SWIFT_PLATFORM_TARGET_PREFIX)/release
|
# xcodeproj.sh runs cargo with both targets and copies their output here.
|
||||||
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]": /tmp/acord/target/aarch64-apple-ios/release
|
||||||
|
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]": /tmp/acord/target/aarch64-apple-ios-sim/release
|
||||||
OTHER_LDFLAGS:
|
OTHER_LDFLAGS:
|
||||||
- -lacord_viewport
|
- -lacord_viewport
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]: x86_64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]: x86_64
|
||||||
|
|
@ -30,8 +32,10 @@ targets:
|
||||||
platform: iOS
|
platform: iOS
|
||||||
sources:
|
sources:
|
||||||
- path: src
|
- path: src
|
||||||
|
- path: Assets.xcassets
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
INFOPLIST_FILE: Info.plist
|
INFOPLIST_FILE: Info.plist
|
||||||
GENERATE_INFOPLIST_FILE: NO
|
GENERATE_INFOPLIST_FILE: NO
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
dependencies: []
|
dependencies: []
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,65 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import os.log
|
||||||
|
import Darwin
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct AcordApp: App {
|
struct AcordApp: App {
|
||||||
|
init() {
|
||||||
|
Self.captureStderr()
|
||||||
|
dlog("AcordApp.init")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pipes Rust staticlib stderr into both NSLog (for Console.app) and the
|
||||||
|
/// real stdout (for `xcrun devicectl --console`, which only forwards
|
||||||
|
/// stdout/stderr). Without this, `eprintln!()` from Rust is silently dropped.
|
||||||
|
private static func captureStderr() {
|
||||||
|
let realStdout = dup(fileno(stdout))
|
||||||
|
guard realStdout != -1 else { return }
|
||||||
|
let outFile = fdopen(realStdout, "w")
|
||||||
|
guard outFile != nil else { close(realStdout); return }
|
||||||
|
setvbuf(outFile, nil, _IONBF, 0)
|
||||||
|
|
||||||
|
var fds: [Int32] = [0, 0]
|
||||||
|
guard pipe(&fds) == 0 else { return }
|
||||||
|
dup2(fds[1], fileno(stderr))
|
||||||
|
setvbuf(stderr, nil, _IONBF, 0)
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .utility).async {
|
||||||
|
guard let f = fdopen(fds[0], "r") else { return }
|
||||||
|
var line: UnsafeMutablePointer<CChar>?
|
||||||
|
var cap: Int = 0
|
||||||
|
while getline(&line, &cap, f) > 0 {
|
||||||
|
if let l = line {
|
||||||
|
fputs("[Rust] ", outFile)
|
||||||
|
fputs(l, outFile)
|
||||||
|
NSLog("[Rust] %s", l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let l = line { free(l) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
.ignoresSafeArea(.keyboard)
|
.onAppear { dlog("AcordApp scene WindowGroup appeared") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
@StateObject private var controller = ViewportController()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
IcedViewportRepresentable()
|
VStack(spacing: 0) {
|
||||||
.ignoresSafeArea()
|
MenuBar(controller: controller)
|
||||||
|
IcedViewportRepresentable(controller: controller)
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.keyboard)
|
||||||
|
.onAppear {
|
||||||
|
dlog("ContentView.onAppear")
|
||||||
|
PermissionsManager.requestSystemPermissions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Gated logging — every diagnostic print in the iOS shell goes through here.
|
||||||
|
/// Release builds compile this to a no-op so no log lines leak into shipping.
|
||||||
|
/// Define DEBUG via `-D DEBUG` when invoking swiftc (debug.sh does this; the
|
||||||
|
/// release path used by install.sh does not).
|
||||||
|
@inline(__always)
|
||||||
|
func dlog(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) {
|
||||||
|
#if DEBUG
|
||||||
|
let stem = (("\(file)" as NSString).lastPathComponent as NSString).deletingPathExtension
|
||||||
|
print("[Acord] \(stem):\(line) — \(message())")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
/// Bridges UIDocumentPickerViewController into the Rust viewport.
|
||||||
|
/// Open and Save flows rely on iOS's per-file permission grant — a
|
||||||
|
/// security-scoped URL is what the picker hands back, and we copy bytes in
|
||||||
|
/// or out under `startAccessingSecurityScopedResource` while it's in scope.
|
||||||
|
enum DocumentPicker {
|
||||||
|
private static var openDelegate: OpenDelegate?
|
||||||
|
private static var saveDelegate: SaveDelegate?
|
||||||
|
|
||||||
|
static func presentOpen(handle: OpaquePointer) {
|
||||||
|
dlog("presentOpen called")
|
||||||
|
guard let root = topViewController() else {
|
||||||
|
dlog("presentOpen: topViewController returned nil — picker NOT shown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// .item is the broadest "any file" UTI — without this, files whose UTI
|
||||||
|
// doesn't exactly match get rendered grey/unselectable in the picker.
|
||||||
|
// asCopy:true sidesteps the security-scoped-resource entitlement dance:
|
||||||
|
// iOS hands us a copy in our sandbox tmp dir we can just read.
|
||||||
|
var types: [UTType] = [.plainText, .utf8PlainText, .text, .sourceCode, .data, .item]
|
||||||
|
if let md = UTType(filenameExtension: "md") { types.insert(md, at: 0) }
|
||||||
|
if let md = UTType("net.daringfireball.markdown") { types.insert(md, at: 0) }
|
||||||
|
dlog("presentOpen: types=\(types.map(\.identifier))")
|
||||||
|
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
|
||||||
|
let delegate = OpenDelegate(handle: handle)
|
||||||
|
openDelegate = delegate
|
||||||
|
picker.delegate = delegate
|
||||||
|
picker.allowsMultipleSelection = false
|
||||||
|
root.present(picker, animated: true) {
|
||||||
|
dlog("presentOpen: picker presented from \(type(of: root))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func presentSave(handle: OpaquePointer, defaultName: String) {
|
||||||
|
dlog("presentSave called")
|
||||||
|
guard let root = topViewController() else {
|
||||||
|
dlog("presentSave: topViewController returned nil — picker NOT shown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cstr = viewport_get_text(handle) else {
|
||||||
|
dlog("presentSave: viewport_get_text returned null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let text = String(cString: cstr)
|
||||||
|
viewport_free_string(cstr)
|
||||||
|
dlog("presentSave: serialized \(text.utf8.count) bytes from viewport")
|
||||||
|
|
||||||
|
let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("\(defaultName).md")
|
||||||
|
do {
|
||||||
|
try text.data(using: .utf8)?.write(to: tmp)
|
||||||
|
dlog("presentSave: wrote tmp \(tmp.path)")
|
||||||
|
} catch {
|
||||||
|
dlog("presentSave: tmp write failed: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let picker = UIDocumentPickerViewController(forExporting: [tmp], asCopy: true)
|
||||||
|
let delegate = SaveDelegate(handle: handle, source: tmp)
|
||||||
|
saveDelegate = delegate
|
||||||
|
picker.delegate = delegate
|
||||||
|
root.present(picker, animated: true) {
|
||||||
|
dlog("presentSave: picker presented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func topViewController() -> UIViewController? {
|
||||||
|
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
|
||||||
|
dlog("topViewController: no UIWindowScene")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let window = scene.windows.first(where: { $0.isKeyWindow }) ?? scene.windows.first else {
|
||||||
|
dlog("topViewController: no window in scene")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var top = window.rootViewController
|
||||||
|
while let presented = top?.presentedViewController { top = presented }
|
||||||
|
return top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class OpenDelegate: NSObject, UIDocumentPickerDelegate {
|
||||||
|
let handle: OpaquePointer
|
||||||
|
init(handle: OpaquePointer) { self.handle = handle }
|
||||||
|
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
dlog("open delegate fired with \(urls.count) urls")
|
||||||
|
guard let url = urls.first else {
|
||||||
|
dlog("open: no url in selection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("open: url=\(url.path)")
|
||||||
|
// asCopy:true means url is already in our sandbox tmp dir — no scoped access needed.
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
guard let text = String(data: data, encoding: .utf8) else {
|
||||||
|
dlog("open: file at \(url.path) is not utf-8 (\(data.count) bytes)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text.withCString { cstr in
|
||||||
|
viewport_set_text(handle, cstr)
|
||||||
|
}
|
||||||
|
dlog("open: loaded \(data.count) bytes (\(text.count) chars) from \(url.lastPathComponent)")
|
||||||
|
} catch {
|
||||||
|
dlog("open: read failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
dlog("open: cancelled by user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SaveDelegate: NSObject, UIDocumentPickerDelegate {
|
||||||
|
let handle: OpaquePointer
|
||||||
|
let source: URL
|
||||||
|
init(handle: OpaquePointer, source: URL) {
|
||||||
|
self.handle = handle
|
||||||
|
self.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
dlog("save: picker resolved with destinations=\(urls.map(\.path))")
|
||||||
|
try? FileManager.default.removeItem(at: source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
dlog("save: cancelled by user")
|
||||||
|
try? FileManager.default.removeItem(at: source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,20 @@ import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// SwiftUI wrapper around the UIView that hosts the Rust viewport.
|
/// SwiftUI wrapper around the UIView that hosts the Rust viewport.
|
||||||
|
/// Stashes the underlying view into the shared ViewportController so the
|
||||||
|
/// menu bar can dispatch commands against the same handle.
|
||||||
struct IcedViewportRepresentable: UIViewRepresentable {
|
struct IcedViewportRepresentable: UIViewRepresentable {
|
||||||
|
let controller: ViewportController
|
||||||
|
|
||||||
func makeUIView(context: Context) -> IcedViewportView {
|
func makeUIView(context: Context) -> IcedViewportView {
|
||||||
IcedViewportView(frame: .zero)
|
dlog("makeUIView")
|
||||||
|
let v = IcedViewportView(frame: .zero)
|
||||||
|
controller.view = v
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: IcedViewportView, context: Context) {
|
func updateUIView(_ uiView: IcedViewportView, context: Context) {
|
||||||
// size pushed via setFrameSize; nothing to refresh per SwiftUI tick.
|
dlog("updateUIView: bounds=\(uiView.bounds.size) handle=\(uiView.viewportHandle != nil ? "live" : "nil")")
|
||||||
|
controller.view = uiView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import UIKit
|
||||||
import QuartzCore
|
import QuartzCore
|
||||||
|
|
||||||
/// CAMetalLayer-backed UIView that owns the Rust viewport handle and pumps
|
/// CAMetalLayer-backed UIView that owns the Rust viewport handle and pumps
|
||||||
/// CADisplayLink ticks into `viewport_render`.
|
/// CADisplayLink ticks into `viewport_render`. UIKeyInput conformance is what
|
||||||
class IcedViewportView: UIView {
|
/// makes the soft keyboard appear when the view becomes first responder.
|
||||||
|
class IcedViewportView: UIView, UIKeyInput {
|
||||||
override class var layerClass: AnyClass { CAMetalLayer.self }
|
override class var layerClass: AnyClass { CAMetalLayer.self }
|
||||||
|
|
||||||
private(set) var viewportHandle: OpaquePointer?
|
private(set) var viewportHandle: OpaquePointer?
|
||||||
|
|
@ -29,15 +30,22 @@ class IcedViewportView: UIView {
|
||||||
metalLayer.framebufferOnly = true
|
metalLayer.framebufferOnly = true
|
||||||
metalLayer.pixelFormat = .bgra8Unorm
|
metalLayer.pixelFormat = .bgra8Unorm
|
||||||
metalLayer.isOpaque = true
|
metalLayer.isOpaque = true
|
||||||
|
dlog("commonInit: scale=\(UIScreen.main.scale) pixelFormat=bgra8Unorm")
|
||||||
|
} else {
|
||||||
|
dlog("commonInit: layer is NOT CAMetalLayer — got \(type(of: layer))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMoveToWindow() {
|
override func didMoveToWindow() {
|
||||||
super.didMoveToWindow()
|
super.didMoveToWindow()
|
||||||
|
dlog("didMoveToWindow: window=\(window != nil ? "set" : "nil") handle=\(viewportHandle != nil ? "live" : "nil") tornDown=\(isTornDown) bounds=\(bounds.size)")
|
||||||
if window != nil && viewportHandle == nil && !isTornDown {
|
if window != nil && viewportHandle == nil && !isTornDown {
|
||||||
createViewport()
|
createViewport()
|
||||||
startDisplayLink()
|
startDisplayLink()
|
||||||
becomeFirstResponder()
|
// intentionally NOT becoming first responder here — claiming it on
|
||||||
|
// appear conflicts with SwiftUI Menu popovers (see the
|
||||||
|
// _UIReparentingView warning when clicking the menu strip).
|
||||||
|
// touchesBegan claims it instead, which is the natural moment.
|
||||||
} else if window == nil {
|
} else if window == nil {
|
||||||
teardown()
|
teardown()
|
||||||
}
|
}
|
||||||
|
|
@ -45,27 +53,66 @@ class IcedViewportView: UIView {
|
||||||
|
|
||||||
override var canBecomeFirstResponder: Bool { true }
|
override var canBecomeFirstResponder: Bool { true }
|
||||||
|
|
||||||
|
// MARK: - UIKeyInput (soft keyboard)
|
||||||
|
|
||||||
|
var hasText: Bool { true }
|
||||||
|
|
||||||
|
func insertText(_ text: String) {
|
||||||
|
guard let h = viewportHandle else {
|
||||||
|
dlog("insertText: no handle — dropped \(text.debugDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("insertText: \(text.debugDescription) (\(text.utf8.count) bytes)")
|
||||||
|
text.withCString { cstr in
|
||||||
|
viewport_key_event(h, 0, 0, true, cstr)
|
||||||
|
viewport_key_event(h, 0, 0, false, cstr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBackward() {
|
||||||
|
guard let h = viewportHandle else {
|
||||||
|
dlog("deleteBackward: no handle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("deleteBackward")
|
||||||
|
"\u{7F}".withCString { cstr in
|
||||||
|
viewport_key_event(h, 51, 0, true, cstr)
|
||||||
|
viewport_key_event(h, 51, 0, false, cstr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func createViewport() {
|
private func createViewport() {
|
||||||
let scale = Float(window?.screen.scale ?? UIScreen.main.scale)
|
let scale = Float(window?.screen.scale ?? UIScreen.main.scale)
|
||||||
let w = Float(bounds.width)
|
let w = Float(bounds.width)
|
||||||
let h = Float(bounds.height)
|
let h = Float(bounds.height)
|
||||||
|
if let metalLayer = layer as? CAMetalLayer, metalLayer.device == nil {
|
||||||
|
metalLayer.device = MTLCreateSystemDefaultDevice()
|
||||||
|
dlog("createViewport: assigned MTLDevice=\(String(describing: metalLayer.device?.name))")
|
||||||
|
}
|
||||||
let viewPtr = Unmanaged.passUnretained(self).toOpaque()
|
let viewPtr = Unmanaged.passUnretained(self).toOpaque()
|
||||||
viewportHandle = viewport_create(viewPtr, w, h, scale)
|
viewportHandle = viewport_create(viewPtr, w, h, scale)
|
||||||
|
dlog("createViewport: bounds=\(w)x\(h) scale=\(scale) handle=\(String(describing: viewportHandle))")
|
||||||
|
if w == 0 || h == 0 {
|
||||||
|
dlog("createViewport: WARNING bounds are zero — layoutSubviews must fire before paint is meaningful")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func destroyViewport() {
|
private func destroyViewport() {
|
||||||
guard let handle = viewportHandle else { return }
|
guard let handle = viewportHandle else { return }
|
||||||
|
dlog("destroyViewport")
|
||||||
viewportHandle = nil
|
viewportHandle = nil
|
||||||
viewport_destroy(handle)
|
viewport_destroy(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func teardown() {
|
func teardown() {
|
||||||
if isTornDown { return }
|
if isTornDown { return }
|
||||||
|
dlog("teardown")
|
||||||
isTornDown = true
|
isTornDown = true
|
||||||
stopDisplayLink()
|
stopDisplayLink()
|
||||||
if let h = viewportHandle, let cstr = viewport_get_text(h) {
|
if let h = viewportHandle, let cstr = viewport_get_text(h) {
|
||||||
cachedText = String(cString: cstr)
|
cachedText = String(cString: cstr)
|
||||||
viewport_free_string(cstr)
|
viewport_free_string(cstr)
|
||||||
|
dlog("teardown: cached \(cachedText.count) chars")
|
||||||
}
|
}
|
||||||
destroyViewport()
|
destroyViewport()
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +126,7 @@ class IcedViewportView: UIView {
|
||||||
let link = CADisplayLink(target: self, selector: #selector(renderFrame))
|
let link = CADisplayLink(target: self, selector: #selector(renderFrame))
|
||||||
link.add(to: .main, forMode: .common)
|
link.add(to: .main, forMode: .common)
|
||||||
displayLink = link
|
displayLink = link
|
||||||
|
dlog("startDisplayLink")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopDisplayLink() {
|
private func stopDisplayLink() {
|
||||||
|
|
@ -86,10 +134,17 @@ class IcedViewportView: UIView {
|
||||||
displayLink = nil
|
displayLink = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var renderCount: Int = 0
|
||||||
@objc private func renderFrame() {
|
@objc private func renderFrame() {
|
||||||
if isTornDown { return }
|
if isTornDown { return }
|
||||||
guard let handle = viewportHandle else { return }
|
guard let handle = viewportHandle else { return }
|
||||||
viewport_render(handle)
|
viewport_render(handle)
|
||||||
|
renderCount += 1
|
||||||
|
// first frame, then 60th (~1s), then every 600 (~10s) to confirm the loop is alive.
|
||||||
|
if renderCount == 1 || renderCount == 60 || renderCount % 600 == 0 {
|
||||||
|
let ml = layer as? CAMetalLayer
|
||||||
|
dlog("renderFrame #\(renderCount) bounds=\(bounds.width)x\(bounds.height) drawable=\(ml?.drawableSize ?? .zero) scale=\(ml?.contentsScale ?? 0)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Resize
|
// MARK: - Resize
|
||||||
|
|
@ -103,8 +158,12 @@ class IcedViewportView: UIView {
|
||||||
width: bounds.width * CGFloat(scale),
|
width: bounds.width * CGFloat(scale),
|
||||||
height: bounds.height * CGFloat(scale)
|
height: bounds.height * CGFloat(scale)
|
||||||
)
|
)
|
||||||
|
dlog("layoutSubviews: bounds=\(bounds.size) scale=\(scale) drawable=\(metalLayer.drawableSize)")
|
||||||
|
}
|
||||||
|
guard let handle = viewportHandle else {
|
||||||
|
dlog("layoutSubviews: no handle yet")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
guard let handle = viewportHandle else { return }
|
|
||||||
viewport_resize(handle, Float(bounds.width), Float(bounds.height), scale)
|
viewport_resize(handle, Float(bounds.width), Float(bounds.height), scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,8 +172,10 @@ class IcedViewportView: UIView {
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
guard let h = viewportHandle, let touch = touches.first else { return }
|
guard let h = viewportHandle, let touch = touches.first else { return }
|
||||||
let p = touch.location(in: self)
|
let p = touch.location(in: self)
|
||||||
|
dlog("touchesBegan: \(p)")
|
||||||
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, true)
|
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, true)
|
||||||
becomeFirstResponder()
|
becomeFirstResponder()
|
||||||
|
dlog("touchesBegan: firstResponder=\(isFirstResponder)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
|
@ -126,12 +187,14 @@ class IcedViewportView: UIView {
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
guard let h = viewportHandle, let touch = touches.first else { return }
|
guard let h = viewportHandle, let touch = touches.first else { return }
|
||||||
let p = touch.location(in: self)
|
let p = touch.location(in: self)
|
||||||
|
dlog("touchesEnded: \(p)")
|
||||||
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, false)
|
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
guard let h = viewportHandle, let touch = touches.first else { return }
|
guard let h = viewportHandle, let touch = touches.first else { return }
|
||||||
let p = touch.location(in: self)
|
let p = touch.location(in: self)
|
||||||
|
dlog("touchesCancelled: \(p)")
|
||||||
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, false)
|
viewport_mouse_event(h, Float(p.x), Float(p.y), 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +223,7 @@ class IcedViewportView: UIView {
|
||||||
private func forwardKey(_ press: UIPress, pressed: Bool, handle: OpaquePointer) {
|
private func forwardKey(_ press: UIPress, pressed: Bool, handle: OpaquePointer) {
|
||||||
guard let key = press.key else { return }
|
guard let key = press.key else { return }
|
||||||
let chars = pressed ? key.characters : ""
|
let chars = pressed ? key.characters : ""
|
||||||
|
dlog("forwardKey: keyCode=\(key.keyCode.rawValue) mods=\(key.modifierFlags.rawValue) pressed=\(pressed) chars=\(chars.debugDescription)")
|
||||||
chars.withCString { cstr in
|
chars.withCString { cstr in
|
||||||
viewport_key_event(handle, UInt32(key.keyCode.rawValue), UInt32(key.modifierFlags.rawValue), pressed, cstr)
|
viewport_key_event(handle, UInt32(key.keyCode.rawValue), UInt32(key.modifierFlags.rawValue), pressed, cstr)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Top toolbar with File / Edit / Render / View menus, mirroring the
|
||||||
|
/// editor's MenuCategory layout. Uses SwiftUI Menu so each label opens a
|
||||||
|
/// dropdown of buttons; each button dispatches through ViewportController.
|
||||||
|
struct MenuBar: View {
|
||||||
|
@ObservedObject var controller: ViewportController
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
fileMenu
|
||||||
|
editMenu
|
||||||
|
renderMenu
|
||||||
|
viewMenu
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color(white: 0.12))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fileMenu: some View {
|
||||||
|
Menu("File") {
|
||||||
|
Button("New Note") { controller.newNote() }
|
||||||
|
Button("Open…") { controller.openDocument() }
|
||||||
|
Divider()
|
||||||
|
Button("Save") { controller.saveDocument() }
|
||||||
|
Button("Save As…") { controller.saveDocumentAs() }
|
||||||
|
Divider()
|
||||||
|
Button("Print…") { controller.printDocument() }
|
||||||
|
Divider()
|
||||||
|
Button("Settings…") { controller.toggleSettings() }
|
||||||
|
}
|
||||||
|
.menuLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
private var editMenu: some View {
|
||||||
|
Menu("Edit") {
|
||||||
|
Button("Undo") { controller.undo() }
|
||||||
|
Button("Redo") { controller.redo() }
|
||||||
|
Divider()
|
||||||
|
Button("Bold") { controller.toggleBold() }
|
||||||
|
Button("Italic") { controller.toggleItalic() }
|
||||||
|
Button("Insert Table") { controller.insertTable() }
|
||||||
|
Divider()
|
||||||
|
Button("Find…") { controller.toggleFind() }
|
||||||
|
}
|
||||||
|
.menuLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
private var renderMenu: some View {
|
||||||
|
Menu("Render") {
|
||||||
|
Button("Live") { controller.setLiveMode() }
|
||||||
|
Button("Editor") { controller.setEditorMode() }
|
||||||
|
Button("View") { controller.setViewMode() }
|
||||||
|
Divider()
|
||||||
|
Button("Evaluate") { controller.evaluate() }
|
||||||
|
}
|
||||||
|
.menuLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
private var viewMenu: some View {
|
||||||
|
Menu("View") {
|
||||||
|
Button("Zoom In") { controller.zoomIn() }
|
||||||
|
Button("Zoom Out") { controller.zoomOut() }
|
||||||
|
Button("Reset Zoom") { controller.resetZoom() }
|
||||||
|
}
|
||||||
|
.menuLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
/// Consistent Menu chrome — slightly padded, light hit target.
|
||||||
|
var menuLabel: some View {
|
||||||
|
self
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import UIKit
|
||||||
|
import Photos
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
/// Drives the iOS permission dialogs that fire on first launch.
|
||||||
|
/// Photos / Camera / Microphone trigger native system prompts when their
|
||||||
|
/// `requestAuthorization` is called, the Info.plist usage strings exist,
|
||||||
|
/// and the corresponding framework is linked.
|
||||||
|
/// File-system access on iOS isn't a global permission — DocumentPicker's
|
||||||
|
/// per-file picker IS the consent moment for that, and asCopy:true means
|
||||||
|
/// no entitlements beyond the picker itself are required.
|
||||||
|
enum PermissionsManager {
|
||||||
|
/// Sequentially asks for Photos → Camera → Microphone access.
|
||||||
|
/// Each call shows the system prompt only when status is .notDetermined.
|
||||||
|
static func requestSystemPermissions() {
|
||||||
|
dlog("requestSystemPermissions called")
|
||||||
|
let photosStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||||||
|
dlog("photos status: \(authStatusName(photosStatus))")
|
||||||
|
if photosStatus == .notDetermined {
|
||||||
|
dlog("photos: requesting authorization (system dialog should appear)")
|
||||||
|
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
||||||
|
dlog("photos: dialog returned \(authStatusName(status))")
|
||||||
|
requestCamera()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dlog("photos: already determined — skipping dialog")
|
||||||
|
requestCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func requestCamera() {
|
||||||
|
let cameraStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
|
dlog("camera status: \(avStatusName(cameraStatus))")
|
||||||
|
if cameraStatus == .notDetermined {
|
||||||
|
dlog("camera: requesting authorization (system dialog should appear)")
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||||
|
dlog("camera: dialog returned granted=\(granted)")
|
||||||
|
requestMicrophone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dlog("camera: already determined — skipping dialog")
|
||||||
|
requestMicrophone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func requestMicrophone() {
|
||||||
|
let micStatus = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||||
|
dlog("microphone status: \(avStatusName(micStatus))")
|
||||||
|
if micStatus == .notDetermined {
|
||||||
|
dlog("microphone: requesting authorization (system dialog should appear)")
|
||||||
|
AVCaptureDevice.requestAccess(for: .audio) { granted in
|
||||||
|
dlog("microphone: dialog returned granted=\(granted)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dlog("microphone: already determined — skipping dialog")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func authStatusName(_ s: PHAuthorizationStatus) -> String {
|
||||||
|
switch s {
|
||||||
|
case .notDetermined: return "notDetermined"
|
||||||
|
case .restricted: return "restricted"
|
||||||
|
case .denied: return "denied"
|
||||||
|
case .authorized: return "authorized"
|
||||||
|
case .limited: return "limited"
|
||||||
|
@unknown default: return "unknown(\(s.rawValue))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func avStatusName(_ s: AVAuthorizationStatus) -> String {
|
||||||
|
switch s {
|
||||||
|
case .notDetermined: return "notDetermined"
|
||||||
|
case .restricted: return "restricted"
|
||||||
|
case .denied: return "denied"
|
||||||
|
case .authorized: return "authorized"
|
||||||
|
@unknown default: return "unknown(\(s.rawValue))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Bridges SwiftUI menu buttons to the Rust viewport handle.
|
||||||
|
/// Holds a weak reference to the IcedViewportView so the menu can dispatch
|
||||||
|
/// commands without owning the rendering surface.
|
||||||
|
final class ViewportController: ObservableObject {
|
||||||
|
weak var view: IcedViewportView?
|
||||||
|
|
||||||
|
func send(_ code: UInt32) {
|
||||||
|
guard let h = view?.viewportHandle else {
|
||||||
|
dlog("send(\(code)): no handle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("send(\(code))")
|
||||||
|
viewport_send_command(h, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editor commands (mirror viewport/src/lib.rs::viewport_send_command codes).
|
||||||
|
func toggleBold() { send(1) }
|
||||||
|
func toggleItalic() { send(2) }
|
||||||
|
func insertTable() { send(3) }
|
||||||
|
func evaluate() { send(5) }
|
||||||
|
func zoomIn() { send(7) }
|
||||||
|
func zoomOut() { send(8) }
|
||||||
|
func resetZoom() { send(9) }
|
||||||
|
func setLiveMode() { send(11) }
|
||||||
|
func setEditorMode() { send(12) }
|
||||||
|
func setViewMode() { send(13) }
|
||||||
|
func toggleSettings() { send(16) }
|
||||||
|
|
||||||
|
/// Hand-rolled key events for shortcuts that flow through iced's text bindings
|
||||||
|
/// rather than the cmd dispatcher (Find, Undo, Redo, etc.).
|
||||||
|
private func sendKey(keyCode: UInt32, modifiers: UInt32, character: String) {
|
||||||
|
guard let h = view?.viewportHandle else {
|
||||||
|
dlog("sendKey: no handle (key=\(character.debugDescription) mods=\(modifiers))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("sendKey: char=\(character.debugDescription) keyCode=\(keyCode) mods=\(modifiers)")
|
||||||
|
character.withCString { cstr in
|
||||||
|
viewport_key_event(h, keyCode, modifiers, true, cstr)
|
||||||
|
viewport_key_event(h, keyCode, modifiers, false, cstr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `f` / cmd. The viewport reads .super_key as cmd via iced's modifier mapping.
|
||||||
|
/// keycode 3 is the macOS keycode for `f`; iced doesn't actually use the
|
||||||
|
/// platform keycode on macOS — it pulls the Key from the characters string.
|
||||||
|
/// So we pass 0 and let the character drive it.
|
||||||
|
func toggleFind() { sendKey(keyCode: 0, modifiers: cmdMask, character: "f") }
|
||||||
|
func undo() { sendKey(keyCode: 0, modifiers: cmdMask, character: "z") }
|
||||||
|
func redo() { sendKey(keyCode: 0, modifiers: cmdMask | shiftMask, character: "Z") }
|
||||||
|
|
||||||
|
// UIKeyModifierFlags bits; copied here so the controller doesn't import UIKit.
|
||||||
|
private var cmdMask: UInt32 { 1 << 20 }
|
||||||
|
private var shiftMask: UInt32 { 1 << 17 }
|
||||||
|
|
||||||
|
/// File operations — Open and Save go through UIDocumentPicker so iOS
|
||||||
|
/// prompts the user to grant per-file access.
|
||||||
|
func newNote() {
|
||||||
|
guard let h = view?.viewportHandle else {
|
||||||
|
dlog("newNote: no handle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("newNote")
|
||||||
|
let stub = "# "
|
||||||
|
stub.withCString { viewport_set_text(h, $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDocument() {
|
||||||
|
guard let h = view?.viewportHandle else {
|
||||||
|
dlog("openDocument: no handle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("openDocument: dispatching to picker")
|
||||||
|
DocumentPicker.presentOpen(handle: h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveDocument() {
|
||||||
|
dlog("saveDocument (routed to saveDocumentAs)")
|
||||||
|
saveDocumentAs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveDocumentAs() {
|
||||||
|
guard let h = view?.viewportHandle else {
|
||||||
|
dlog("saveDocumentAs: no handle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dlog("saveDocumentAs: dispatching to picker")
|
||||||
|
DocumentPicker.presentSave(handle: h, defaultName: "Acord")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDocument() {
|
||||||
|
dlog("printDocument: not implemented")
|
||||||
|
// TODO: call viewport_render_pdf, hand to UIPrintInteractionController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sourced by build / install / debug scripts to redirect cargo's target dir
|
||||||
|
# to the boot SSD instead of the repo's external spinning disk.
|
||||||
|
# Scripts read compiled artifacts from $CARGO_TARGET_DIR and copy the final
|
||||||
|
# .app / .exe / binary into $ROOT/build/ as real files at the end.
|
||||||
|
#
|
||||||
|
# Override the SSD location with: export CARGO_TARGET_DIR=/some/other/path
|
||||||
|
|
||||||
|
if [ -n "${ACORD_BUILD_DIRS_DONE:-}" ]; then
|
||||||
|
return 0 2>/dev/null || exit 0
|
||||||
|
fi
|
||||||
|
export ACORD_BUILD_DIRS_DONE=1
|
||||||
|
|
||||||
|
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-/tmp/acord/target}"
|
||||||
|
mkdir -p "$CARGO_TARGET_DIR"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Stub — the Android shell hasn't been started yet.
|
||||||
|
# When it lands, this should mirror scripts/ios/select.sh: list `adb devices`
|
||||||
|
# entries plus available emulators (`emulator -list-avds`), let the user
|
||||||
|
# pick one, and write the choice to $HOME/.acord/android-target.
|
||||||
|
|
||||||
|
cat <<'EOF' >&2
|
||||||
|
android select is a stub — the Android shell isn't built yet.
|
||||||
|
|
||||||
|
when it ships, this will:
|
||||||
|
- list connected devices (adb devices)
|
||||||
|
- list available emulators (emulator -list-avds)
|
||||||
|
- write the picked target to $HOME/.acord/android-target
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -29,9 +30,29 @@ case "$TARGET" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# debug.sh sets ACORD_IOS_CONFIG=debug to flip cargo to the dev profile and
|
||||||
|
# pass -D DEBUG / -Onone / -g to swiftc. install.sh leaves it unset → release.
|
||||||
|
CONFIG="${ACORD_IOS_CONFIG:-release}"
|
||||||
|
case "$CONFIG" in
|
||||||
|
release)
|
||||||
|
CARGO_FLAGS=(--release)
|
||||||
|
CARGO_PROFILE_DIR="release"
|
||||||
|
SWIFT_OPT_FLAGS=(-O)
|
||||||
|
;;
|
||||||
|
debug)
|
||||||
|
CARGO_FLAGS=()
|
||||||
|
CARGO_PROFILE_DIR="debug"
|
||||||
|
SWIFT_OPT_FLAGS=(-Onone -g -D DEBUG)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ACORD_IOS_CONFIG must be release or debug (got '$CONFIG')" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
BUILD="$ROOT/build"
|
BUILD="$ROOT/build"
|
||||||
APP="$BUILD/ios/Acord.app"
|
APP="$BUILD/ios/Acord.app"
|
||||||
RUST_LIB="$ROOT/target/$RUST_TARGET/release"
|
RUST_LIB="$CARGO_TARGET_DIR/$RUST_TARGET/$CARGO_PROFILE_DIR"
|
||||||
|
|
||||||
SDK="$(xcrun --sdk "$SDK_NAME" --show-sdk-path)"
|
SDK="$(xcrun --sdk "$SDK_NAME" --show-sdk-path)"
|
||||||
|
|
||||||
|
|
@ -40,8 +61,8 @@ export CC=/usr/bin/clang
|
||||||
export CXX=/usr/bin/clang++
|
export CXX=/usr/bin/clang++
|
||||||
export IPHONEOS_DEPLOYMENT_TARGET=17.0
|
export IPHONEOS_DEPLOYMENT_TARGET=17.0
|
||||||
|
|
||||||
echo "Building Rust workspace for $RUST_TARGET (release)..."
|
echo "Building Rust workspace for $RUST_TARGET ($CONFIG)..."
|
||||||
cargo build --release --target "$RUST_TARGET" -p acord-viewport
|
cargo build "${CARGO_FLAGS[@]}" --target "$RUST_TARGET" -p acord-viewport
|
||||||
|
|
||||||
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
||||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB" >&2
|
echo "ERROR: libacord_viewport.a not found at $RUST_LIB" >&2
|
||||||
|
|
@ -53,18 +74,32 @@ rm -rf "$APP"
|
||||||
mkdir -p "$APP"
|
mkdir -p "$APP"
|
||||||
cp "$ROOT/ios/Info.plist" "$APP/Info.plist"
|
cp "$ROOT/ios/Info.plist" "$APP/Info.plist"
|
||||||
|
|
||||||
# generate icon (PNG variants required by iOS) — single 1024 master, scaled to bundle entries.
|
# asset catalog → compiled Assets.car + partial Info.plist via actool.
|
||||||
SVG="$ROOT/assets/Acord.svg"
|
# regenerate the source PNGs from the SVG so a fresh checkout doesn't need
|
||||||
if [ -f "$SVG" ] && command -v rsvg-convert >/dev/null 2>&1; then
|
# a manual icon-rebuild step.
|
||||||
echo "Generating app icons..."
|
bash "$ROOT/scripts/ios/generate-icons.sh"
|
||||||
for size in 20 29 40 58 60 76 80 87 120 152 167 180 1024; do
|
|
||||||
rsvg-convert --width="$size" --height="$size" "$SVG" -o "$APP/AppIcon-${size}.png"
|
ACTOOL_PARTIAL="$BUILD/ios/actool-partial-info.plist"
|
||||||
done
|
mkdir -p "$BUILD/ios"
|
||||||
|
echo "Compiling asset catalog..."
|
||||||
|
xcrun actool "$ROOT/ios/Assets.xcassets" \
|
||||||
|
--compile "$APP" \
|
||||||
|
--platform "$SDK_NAME" \
|
||||||
|
--minimum-deployment-target 17.0 \
|
||||||
|
--app-icon AppIcon \
|
||||||
|
--output-partial-info-plist "$ACTOOL_PARTIAL" \
|
||||||
|
--target-device ipad \
|
||||||
|
--target-device iphone \
|
||||||
|
>/dev/null
|
||||||
|
|
||||||
|
# merge the actool partial plist (icon name + variants) into Info.plist.
|
||||||
|
if [ -f "$ACTOOL_PARTIAL" ]; then
|
||||||
|
/usr/libexec/PlistBuddy -c "Merge $ACTOOL_PARTIAL" "$APP/Info.plist" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
||||||
|
|
||||||
echo "Compiling Swift (release)..."
|
echo "Compiling Swift ($CONFIG)..."
|
||||||
xcrun -sdk "$SDK_NAME" swiftc \
|
xcrun -sdk "$SDK_NAME" swiftc \
|
||||||
-target "$SWIFT_TARGET" \
|
-target "$SWIFT_TARGET" \
|
||||||
-sdk "$SDK" \
|
-sdk "$SDK" \
|
||||||
|
|
@ -76,7 +111,10 @@ xcrun -sdk "$SDK_NAME" swiftc \
|
||||||
-framework MetalKit \
|
-framework MetalKit \
|
||||||
-framework CoreGraphics \
|
-framework CoreGraphics \
|
||||||
-framework CoreFoundation \
|
-framework CoreFoundation \
|
||||||
-O \
|
-framework Photos \
|
||||||
|
-framework AVFoundation \
|
||||||
|
-framework UniformTypeIdentifiers \
|
||||||
|
"${SWIFT_OPT_FLAGS[@]}" \
|
||||||
-o "$APP/Acord" \
|
-o "$APP/Acord" \
|
||||||
"$ROOT"/ios/src/*.swift
|
"$ROOT"/ios/src/*.swift
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,85 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Builds debug, installs to the chosen target, and launches with stdio attached
|
||||||
|
# so Swift print() AND Rust eprintln!() (via captureStderr in AcordApp) stream
|
||||||
|
# straight into this terminal.
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
# debug build: skip release flags by reusing build.sh but with the dev profile.
|
export ACORD_IOS_CONFIG=debug
|
||||||
# build.sh always uses release; for now, point users to install.sh for normal use,
|
|
||||||
# and stream the simulator log for the bundle id.
|
CONFIG_FILE="$HOME/.acord/ios-target"
|
||||||
bash "$ROOT/scripts/ios/install.sh"
|
KIND="" ; ID=""
|
||||||
xcrun simctl spawn booted log stream --predicate 'subsystem == "org.else-if.acord" OR processImagePath CONTAINS "Acord"' --level debug
|
if [ -n "${1:-}" ]; then
|
||||||
|
case "$1" in
|
||||||
|
sim|device) KIND="$1";;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [ -z "$KIND" ] && [ -f "$CONFIG_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
if [ -z "$KIND" ]; then
|
||||||
|
if xcrun devicectl list devices 2>/dev/null | grep -q "available (paired)"; then
|
||||||
|
KIND="device"
|
||||||
|
else
|
||||||
|
KIND="sim"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$ID" ]; then
|
||||||
|
case "$KIND" in
|
||||||
|
device)
|
||||||
|
ID="$(xcrun devicectl list devices 2>/dev/null \
|
||||||
|
| awk '/available \(paired\)/ {for(i=1;i<=NF;i++) if($i ~ /^[A-F0-9-]{36}$/) {print $i; exit}}')"
|
||||||
|
;;
|
||||||
|
sim)
|
||||||
|
ID="$(xcrun simctl list devices booted | { grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' || true; } | head -1)"
|
||||||
|
if [ -z "$ID" ]; then
|
||||||
|
ID="$(xcrun simctl list devices available | awk '/iPad/' | { grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' || true; } | head -1)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
bash "$ROOT/scripts/ios/build.sh" "$KIND"
|
||||||
|
|
||||||
|
APP="$ROOT/build/ios/Acord.app"
|
||||||
|
BUNDLE_ID="org.else-if.acord"
|
||||||
|
|
||||||
|
case "$KIND" in
|
||||||
|
device)
|
||||||
|
if [ -z "$ID" ]; then
|
||||||
|
echo "no paired device found — connect via cable and trust this Mac" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Installing to device $ID..."
|
||||||
|
xcrun devicectl device install app --device "$ID" "$APP"
|
||||||
|
echo "Launching with live console (Ctrl+C to detach)..."
|
||||||
|
echo "----------------------------------------------------------"
|
||||||
|
exec xcrun devicectl device process launch \
|
||||||
|
--device "$ID" \
|
||||||
|
--console \
|
||||||
|
--terminate-existing \
|
||||||
|
"$BUNDLE_ID"
|
||||||
|
;;
|
||||||
|
sim)
|
||||||
|
if [ -z "$ID" ]; then
|
||||||
|
echo "no iPad simulator available — open Xcode → Window → Devices and Simulators to add one" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
xcrun simctl boot "$ID" 2>/dev/null || true
|
||||||
|
open -a Simulator
|
||||||
|
echo "Installing to simulator $ID..."
|
||||||
|
xcrun simctl install "$ID" "$APP"
|
||||||
|
echo "Launching with live console (Ctrl+C to detach)..."
|
||||||
|
echo "----------------------------------------------------------"
|
||||||
|
exec xcrun simctl launch --console-pty --terminate-running-process "$ID" "$BUNDLE_ID"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown KIND='$KIND'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generates ios/Assets.xcassets/AppIcon.appiconset/ from assets/Acord.svg.
|
||||||
|
# Used by both the CLI build (build.sh) and the Xcode project path
|
||||||
|
# (xcodeproj.sh). Idempotent — re-running just overwrites.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
SVG="$ROOT/assets/Acord.svg"
|
||||||
|
ASSETS="$ROOT/ios/Assets.xcassets"
|
||||||
|
APPICON="$ASSETS/AppIcon.appiconset"
|
||||||
|
|
||||||
|
if [ ! -f "$SVG" ]; then
|
||||||
|
echo "ERROR: $SVG not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v rsvg-convert >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: rsvg-convert not on PATH (brew install librsvg or port install librsvg)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$APPICON"
|
||||||
|
|
||||||
|
# (filename, pixel size) pairs covering iPhone + iPad icon slots through iOS 17.
|
||||||
|
# 1024 is the marketing icon; the rest are point-size@scale variants.
|
||||||
|
SIZES=(
|
||||||
|
"Icon-20.png 20"
|
||||||
|
"Icon-20@2x.png 40"
|
||||||
|
"Icon-20@3x.png 60"
|
||||||
|
"Icon-29.png 29"
|
||||||
|
"Icon-29@2x.png 58"
|
||||||
|
"Icon-29@3x.png 87"
|
||||||
|
"Icon-40.png 40"
|
||||||
|
"Icon-40@2x.png 80"
|
||||||
|
"Icon-40@3x.png 120"
|
||||||
|
"Icon-60@2x.png 120"
|
||||||
|
"Icon-60@3x.png 180"
|
||||||
|
"Icon-76.png 76"
|
||||||
|
"Icon-76@2x.png 152"
|
||||||
|
"Icon-83.5@2x.png 167"
|
||||||
|
"Icon-1024.png 1024"
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in "${SIZES[@]}"; do
|
||||||
|
name="${entry%% *}"
|
||||||
|
size="${entry##* }"
|
||||||
|
rsvg-convert --width="$size" --height="$size" "$SVG" -o "$APPICON/$name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Top-level Assets.xcassets/Contents.json (xcode requires it even if empty-ish).
|
||||||
|
cat > "$ASSETS/Contents.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# AppIcon.appiconset/Contents.json — maps every Icon-*.png to its slot.
|
||||||
|
cat > "$APPICON/Contents.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "iphone", "size" : "20x20", "scale" : "2x", "filename" : "Icon-20@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "20x20", "scale" : "3x", "filename" : "Icon-20@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "29x29", "scale" : "2x", "filename" : "Icon-29@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "29x29", "scale" : "3x", "filename" : "Icon-29@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "40x40", "scale" : "2x", "filename" : "Icon-40@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "40x40", "scale" : "3x", "filename" : "Icon-40@3x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "60x60", "scale" : "2x", "filename" : "Icon-60@2x.png" },
|
||||||
|
{ "idiom" : "iphone", "size" : "60x60", "scale" : "3x", "filename" : "Icon-60@3x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "20x20", "scale" : "1x", "filename" : "Icon-20.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "20x20", "scale" : "2x", "filename" : "Icon-20@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "29x29", "scale" : "1x", "filename" : "Icon-29.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "29x29", "scale" : "2x", "filename" : "Icon-29@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "40x40", "scale" : "1x", "filename" : "Icon-40.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "40x40", "scale" : "2x", "filename" : "Icon-40@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "76x76", "scale" : "1x", "filename" : "Icon-76.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "76x76", "scale" : "2x", "filename" : "Icon-76@2x.png" },
|
||||||
|
{ "idiom" : "ipad", "size" : "83.5x83.5","scale" : "2x","filename" : "Icon-83.5@2x.png" },
|
||||||
|
{ "idiom" : "ios-marketing","size" : "1024x1024","scale" : "1x","filename" : "Icon-1024.png" }
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Wrote $APPICON ($(ls "$APPICON" | wc -l | tr -d ' ') files)"
|
||||||
|
|
@ -2,10 +2,32 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
# pick the deploy target: explicit arg, or auto-detect (paired physical device wins).
|
CONFIG_FILE="$HOME/.acord/ios-target"
|
||||||
TARGET="${1:-}"
|
|
||||||
|
# resolve target. priority:
|
||||||
|
# 1. explicit cli arg ("sim" / "device")
|
||||||
|
# 2. saved selection from `cargo xtask select-ios`
|
||||||
|
# 3. auto-detect (paired physical device wins)
|
||||||
|
TARGET=""
|
||||||
|
SELECTED_ID=""
|
||||||
|
SELECTED_LABEL=""
|
||||||
|
|
||||||
|
CLI_ARG="${1:-}"
|
||||||
|
if [ -n "$CLI_ARG" ]; then
|
||||||
|
TARGET="$CLI_ARG"
|
||||||
|
elif [ -f "$CONFIG_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG_FILE"
|
||||||
|
case "${KIND:-}" in
|
||||||
|
device) TARGET="device"; SELECTED_ID="$ID"; SELECTED_LABEL="${LABEL:-}";;
|
||||||
|
sim) TARGET="sim"; SELECTED_ID="$ID"; SELECTED_LABEL="${LABEL:-}";;
|
||||||
|
*) echo "warning: $CONFIG_FILE has unknown KIND='${KIND:-}', falling back to auto-detect" >&2;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$TARGET" ]; then
|
if [ -z "$TARGET" ]; then
|
||||||
if xcrun devicectl list devices 2>/dev/null | grep -q "available (paired)"; then
|
if xcrun devicectl list devices 2>/dev/null | grep -q "available (paired)"; then
|
||||||
TARGET="device"
|
TARGET="device"
|
||||||
|
|
@ -14,34 +36,52 @@ if [ -z "$TARGET" ]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "$SELECTED_LABEL" ]; then
|
||||||
|
echo "Target: $TARGET — $SELECTED_LABEL ($SELECTED_ID)"
|
||||||
|
else
|
||||||
|
echo "Target: $TARGET (auto-detected)"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$TARGET" in
|
case "$TARGET" in
|
||||||
sim)
|
sim)
|
||||||
bash "$ROOT/scripts/ios/build.sh" sim
|
bash "$ROOT/scripts/ios/build.sh" sim
|
||||||
APP="$ROOT/build/ios/Acord.app"
|
APP="$ROOT/build/ios/Acord.app"
|
||||||
BUNDLE_ID="org.else-if.acord"
|
BUNDLE_ID="org.else-if.acord"
|
||||||
|
|
||||||
DEV="$(xcrun simctl list devices booted | awk '/Booted/ {print $NF}' | tr -d '()' | head -1 || true)"
|
DEV="$SELECTED_ID"
|
||||||
if [ -z "$DEV" ]; then
|
if [ -z "$DEV" ]; then
|
||||||
DEV="$(xcrun simctl list devices available | awk '/iPad/ && /\([A-F0-9\-]+\)/ {gsub(/[\(\)]/,"",$NF); print $NF; exit}')"
|
DEV="$(xcrun simctl list devices booted | { grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' || true; } | head -1)"
|
||||||
|
fi
|
||||||
|
if [ -z "$DEV" ]; then
|
||||||
|
DEV="$(xcrun simctl list devices available | awk '/iPad/' | { grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' || true; } | head -1)"
|
||||||
if [ -z "$DEV" ]; then
|
if [ -z "$DEV" ]; then
|
||||||
echo "no iPad simulator available — open Xcode → Window → Devices and Simulators to add one" >&2
|
echo "no iPad simulator available — open Xcode → Window → Devices and Simulators to add one" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# boot it if it isn't already.
|
||||||
|
STATE="$(xcrun simctl list devices | awk -v id="$DEV" '$0 ~ id { for (i=1; i<=NF; i++) if ($i ~ /^\(/) state=$i } END { gsub(/[()]/, "", state); print state }')"
|
||||||
|
if [ "$STATE" != "Booted" ]; then
|
||||||
xcrun simctl boot "$DEV" 2>/dev/null || true
|
xcrun simctl boot "$DEV" 2>/dev/null || true
|
||||||
open -a Simulator
|
open -a Simulator
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Installing to simulator $DEV..."
|
echo "Installing to simulator $DEV..."
|
||||||
xcrun simctl install "$DEV" "$APP"
|
xcrun simctl install "$DEV" "$APP"
|
||||||
echo "Launching..."
|
if [ "${ACORD_IOS_NOLAUNCH:-0}" = "1" ]; then
|
||||||
xcrun simctl launch "$DEV" "$BUNDLE_ID"
|
echo "Skipping launch (ACORD_IOS_NOLAUNCH=1)"
|
||||||
|
else
|
||||||
|
echo "Launching..."
|
||||||
|
xcrun simctl launch "$DEV" "$BUNDLE_ID"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
device)
|
device)
|
||||||
bash "$ROOT/scripts/ios/build.sh" device
|
bash "$ROOT/scripts/ios/build.sh" device
|
||||||
APP="$ROOT/build/ios/Acord.app"
|
APP="$ROOT/build/ios/Acord.app"
|
||||||
|
|
||||||
DEVICE_ID="${ACORD_IOS_DEVICE:-}"
|
DEVICE_ID="${SELECTED_ID:-${ACORD_IOS_DEVICE:-}}"
|
||||||
if [ -z "$DEVICE_ID" ]; then
|
if [ -z "$DEVICE_ID" ]; then
|
||||||
DEVICE_ID="$(xcrun devicectl list devices 2>/dev/null \
|
DEVICE_ID="$(xcrun devicectl list devices 2>/dev/null \
|
||||||
| awk '/available \(paired\)/ {for(i=1;i<=NF;i++) if($i ~ /^[A-F0-9-]{36}$/) {print $i; exit}}')"
|
| awk '/available \(paired\)/ {for(i=1;i<=NF;i++) if($i ~ /^[A-F0-9-]{36}$/) {print $i; exit}}')"
|
||||||
|
|
@ -49,14 +89,19 @@ case "$TARGET" in
|
||||||
|
|
||||||
if [ -z "$DEVICE_ID" ]; then
|
if [ -z "$DEVICE_ID" ]; then
|
||||||
echo "no paired device found — connect via cable and trust this Mac on the device" >&2
|
echo "no paired device found — connect via cable and trust this Mac on the device" >&2
|
||||||
|
echo " or run: cargo xtask select-ios" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Installing to device $DEVICE_ID..."
|
echo "Installing to device $DEVICE_ID..."
|
||||||
xcrun devicectl device install app --device "$DEVICE_ID" "$APP"
|
xcrun devicectl device install app --device "$DEVICE_ID" "$APP"
|
||||||
|
|
||||||
echo "Launching..."
|
if [ "${ACORD_IOS_NOLAUNCH:-0}" = "1" ]; then
|
||||||
xcrun devicectl device process launch --device "$DEVICE_ID" org.else-if.acord || true
|
echo "Skipping launch (ACORD_IOS_NOLAUNCH=1)"
|
||||||
|
else
|
||||||
|
echo "Launching..."
|
||||||
|
xcrun devicectl device process launch --device "$DEVICE_ID" org.else-if.acord || true
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Interactive picker for the iOS deploy target. Lists every paired physical
|
||||||
|
# device and every available iPad simulator, lets the user pick one, then
|
||||||
|
# stores the choice at $HOME/.acord/ios-target so install.sh / debug.sh can
|
||||||
|
# read it back without prompting again.
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.acord"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/ios-target"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
|
ALL_FILE="$(mktemp)"
|
||||||
|
trap 'rm -f "$ALL_FILE"' EXIT
|
||||||
|
|
||||||
|
echo "Scanning paired devices..."
|
||||||
|
# devicectl columns are aligned with 2+ spaces. fields: name | host | uuid | state | model
|
||||||
|
xcrun devicectl list devices 2>/dev/null \
|
||||||
|
| awk -F' +' '/available \(paired\)/ {
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $3)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $5)
|
||||||
|
if ($3 ~ /^[A-F0-9-]{36}$/) print "device|" $3 "|" $1 " — " $5
|
||||||
|
}' >> "$ALL_FILE" || true
|
||||||
|
|
||||||
|
echo "Scanning iPad simulators..."
|
||||||
|
# simctl line: " iPad Pro 11-inch (M4) (UUID) (Shutdown)"
|
||||||
|
# strip whitespace, peel off "(state)" then "(uuid)" from the right.
|
||||||
|
xcrun simctl list devices available 2>/dev/null \
|
||||||
|
| awk '/iPad/ {
|
||||||
|
line=$0
|
||||||
|
sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line)
|
||||||
|
if (match(line, /\([^()]+\)$/)) {
|
||||||
|
state=substr(line, RSTART+1, RLENGTH-2)
|
||||||
|
line=substr(line, 1, RSTART-1)
|
||||||
|
sub(/[[:space:]]+$/, "", line)
|
||||||
|
} else { state="" }
|
||||||
|
if (match(line, /\([A-F0-9-]{36}\)$/)) {
|
||||||
|
uuid=substr(line, RSTART+1, 36)
|
||||||
|
name=substr(line, 1, RSTART-1)
|
||||||
|
sub(/[[:space:]]+$/, "", name)
|
||||||
|
} else { next }
|
||||||
|
print "sim|" uuid "|" name " (" state ")"
|
||||||
|
}' >> "$ALL_FILE" || true
|
||||||
|
|
||||||
|
COUNT=$(wc -l < "$ALL_FILE" | tr -d ' ')
|
||||||
|
if [ "$COUNT" -eq 0 ]; then
|
||||||
|
echo "no paired devices and no iPad simulators found" >&2
|
||||||
|
echo " - connect an iPad via cable and trust this Mac" >&2
|
||||||
|
echo " - or open Xcode → Window → Devices and Simulators to add a sim" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "available iOS targets:"
|
||||||
|
i=1
|
||||||
|
while IFS= read -r entry; do
|
||||||
|
[ -z "$entry" ] && continue
|
||||||
|
IFS='|' read -r kind id label <<< "$entry"
|
||||||
|
printf " %2d) [%-6s] %s\n" "$i" "$kind" "$label"
|
||||||
|
i=$((i + 1))
|
||||||
|
done < "$ALL_FILE"
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -r -p "pick a target (number): " CHOICE
|
||||||
|
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "$COUNT" ]; then
|
||||||
|
echo "invalid choice: $CHOICE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PICK=$(sed -n "${CHOICE}p" "$ALL_FILE")
|
||||||
|
IFS='|' read -r KIND ID LABEL <<< "$PICK"
|
||||||
|
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
|
KIND=$KIND
|
||||||
|
ID=$ID
|
||||||
|
LABEL=$LABEL
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "saved $CONFIG_FILE: $KIND $ID ($LABEL)"
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
if ! command -v xcodegen >/dev/null 2>&1; then
|
if ! command -v xcodegen >/dev/null 2>&1; then
|
||||||
|
|
@ -20,6 +21,9 @@ echo "Building Rust staticlibs for both iOS targets (release)..."
|
||||||
cargo build --release --target aarch64-apple-ios -p acord-viewport
|
cargo build --release --target aarch64-apple-ios -p acord-viewport
|
||||||
cargo build --release --target aarch64-apple-ios-sim -p acord-viewport
|
cargo build --release --target aarch64-apple-ios-sim -p acord-viewport
|
||||||
|
|
||||||
|
# regenerate the asset catalog so xcode picks up the latest svg.
|
||||||
|
bash "$ROOT/scripts/ios/generate-icons.sh"
|
||||||
|
|
||||||
cd "$ROOT/ios"
|
cd "$ROOT/ios"
|
||||||
echo "Generating Acord.xcodeproj..."
|
echo "Generating Acord.xcodeproj..."
|
||||||
xcodegen generate
|
xcodegen generate
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -37,7 +38,7 @@ fi
|
||||||
STAGE="$ROOT/build/bin"
|
STAGE="$ROOT/build/bin"
|
||||||
mkdir -p "$STAGE"
|
mkdir -p "$STAGE"
|
||||||
|
|
||||||
cp "$ROOT/target/release/acord" "$STAGE/Acord"
|
cp "$CARGO_TARGET_DIR/release/acord" "$STAGE/Acord"
|
||||||
chmod +x "$STAGE/Acord"
|
chmod +x "$STAGE/Acord"
|
||||||
|
|
||||||
# Rasterize the SVG icon next to the binary so load_window_icon picks it up.
|
# Rasterize the SVG icon next to the binary so load_window_icon picks it up.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ set -euo pipefail
|
||||||
# launched in the foreground so Rust panics print straight to this terminal.
|
# launched in the foreground so Rust panics print straight to this terminal.
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -20,11 +21,11 @@ else
|
||||||
cargo build -p acord-linux
|
cargo build -p acord-linux
|
||||||
fi
|
fi
|
||||||
|
|
||||||
EXE="$ROOT/target/debug/acord"
|
EXE="$CARGO_TARGET_DIR/debug/acord"
|
||||||
|
|
||||||
# Rasterize the icon next to the exe so the dev binary has a window icon too.
|
# Rasterize the icon next to the exe so the dev binary has a window icon too.
|
||||||
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Acord.svg" ]; then
|
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Acord.svg" ]; then
|
||||||
rsvg-convert --width 256 --height 256 "$ROOT/assets/Acord.svg" -o "$ROOT/target/debug/icon.png"
|
rsvg-convert --width 256 --height 256 "$ROOT/assets/Acord.svg" -o "$CARGO_TARGET_DIR/debug/icon.png"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pkill -x acord 2>/dev/null || true
|
pkill -x acord 2>/dev/null || true
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -23,11 +24,11 @@ rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
cargo build --release -p acord-viewport --target aarch64-apple-darwin
|
cargo build --release -p acord-viewport --target aarch64-apple-darwin
|
||||||
cargo build --release -p acord-viewport --target x86_64-apple-darwin
|
cargo build --release -p acord-viewport --target x86_64-apple-darwin
|
||||||
|
|
||||||
mkdir -p "$ROOT/target/universal"
|
mkdir -p "$CARGO_TARGET_DIR/universal"
|
||||||
lipo -create \
|
lipo -create \
|
||||||
"$ROOT/target/aarch64-apple-darwin/release/libacord_viewport.a" \
|
"$CARGO_TARGET_DIR/aarch64-apple-darwin/release/libacord_viewport.a" \
|
||||||
"$ROOT/target/x86_64-apple-darwin/release/libacord_viewport.a" \
|
"$CARGO_TARGET_DIR/x86_64-apple-darwin/release/libacord_viewport.a" \
|
||||||
-output "$ROOT/target/universal/libacord_viewport.a"
|
-output "$CARGO_TARGET_DIR/universal/libacord_viewport.a"
|
||||||
|
|
||||||
# TODO: regenerate AppIcon.icns from assets/Acord.svg here (see build.sh).
|
# TODO: regenerate AppIcon.icns from assets/Acord.svg here (see build.sh).
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ cp "$ROOT/macos/Info.plist" "$CONTENTS/Info.plist"
|
||||||
|
|
||||||
echo "Compiling Swift (Universal)..."
|
echo "Compiling Swift (Universal)..."
|
||||||
SWIFT_FILES=("$ROOT"/macos/src/*.swift)
|
SWIFT_FILES=("$ROOT"/macos/src/*.swift)
|
||||||
RUST_INCLUDES=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$ROOT/target/universal" -lacord_viewport)
|
RUST_INCLUDES=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$CARGO_TARGET_DIR/universal" -lacord_viewport)
|
||||||
|
|
||||||
swiftc -target arm64-apple-macosx14.0 -sdk "$SDK" "${RUST_INCLUDES[@]}" \
|
swiftc -target arm64-apple-macosx14.0 -sdk "$SDK" "${RUST_INCLUDES[@]}" \
|
||||||
-framework Cocoa -framework SwiftUI -framework Metal -framework MetalKit -O \
|
-framework Cocoa -framework SwiftUI -framework Metal -framework MetalKit -O \
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -16,7 +17,7 @@ MACOS="$CONTENTS/MacOS"
|
||||||
RESOURCES="$CONTENTS/Resources"
|
RESOURCES="$CONTENTS/Resources"
|
||||||
|
|
||||||
SDK=$(xcrun --show-sdk-path)
|
SDK=$(xcrun --show-sdk-path)
|
||||||
RUST_LIB="$ROOT/target/release"
|
RUST_LIB="$CARGO_TARGET_DIR/release"
|
||||||
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||||
export ZERO_AR_DATE=0
|
export ZERO_AR_DATE=0
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ set -euo pipefail
|
||||||
# (the panic hook in viewport/src/lib.rs flushes stderr before SIGABRT).
|
# (the panic hook in viewport/src/lib.rs flushes stderr before SIGABRT).
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -20,7 +21,7 @@ MACOS="$CONTENTS/MacOS"
|
||||||
RESOURCES="$CONTENTS/Resources"
|
RESOURCES="$CONTENTS/Resources"
|
||||||
|
|
||||||
SDK=$(xcrun --show-sdk-path)
|
SDK=$(xcrun --show-sdk-path)
|
||||||
RUST_LIB="$ROOT/target/debug"
|
RUST_LIB="$CARGO_TARGET_DIR/debug"
|
||||||
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||||
export ZERO_AR_DATE=0
|
export ZERO_AR_DATE=0
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ set -euo pipefail
|
||||||
# cargo install cargo-zigbuild
|
# cargo install cargo-zigbuild
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
source "$ROOT/scripts/_build-dirs.sh"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -141,7 +142,7 @@ build_macos() {
|
||||||
export ZERO_AR_DATE=0
|
export ZERO_AR_DATE=0
|
||||||
cargo build --release -p acord-viewport --target "$rust_target"
|
cargo build --release -p acord-viewport --target "$rust_target"
|
||||||
|
|
||||||
local rust_lib="$ROOT/target/$rust_target/release"
|
local rust_lib="$CARGO_TARGET_DIR/$rust_target/release"
|
||||||
[ -f "$rust_lib/libacord_viewport.a" ] \
|
[ -f "$rust_lib/libacord_viewport.a" ] \
|
||||||
|| { echo "ERROR: libacord_viewport.a missing for $rust_target" >&2; exit 1; }
|
|| { echo "ERROR: libacord_viewport.a missing for $rust_target" >&2; exit 1; }
|
||||||
|
|
||||||
|
|
@ -187,7 +188,7 @@ build_windows() {
|
||||||
rm -rf "$stage"
|
rm -rf "$stage"
|
||||||
mkdir -p "$stage"
|
mkdir -p "$stage"
|
||||||
|
|
||||||
cp "$ROOT/target/$rust_target/release/acord.exe" "$stage/Acord.exe"
|
cp "$CARGO_TARGET_DIR/$rust_target/release/acord.exe" "$stage/Acord.exe"
|
||||||
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
||||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
||||||
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
|
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
|
||||||
|
|
@ -214,7 +215,7 @@ build_linux() {
|
||||||
rm -rf "$stage"
|
rm -rf "$stage"
|
||||||
mkdir -p "$stage"
|
mkdir -p "$stage"
|
||||||
|
|
||||||
cp "$ROOT/target/$rust_target/release/acord" "$stage/Acord"
|
cp "$CARGO_TARGET_DIR/$rust_target/release/acord" "$stage/Acord"
|
||||||
chmod +x "$stage/Acord"
|
chmod +x "$stage/Acord"
|
||||||
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
|
||||||
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,12 @@ pub enum Message {
|
||||||
InlineResultRelease,
|
InlineResultRelease,
|
||||||
/// double-click on an inline result
|
/// double-click on an inline result
|
||||||
InlineResultDoubleClick { block_id: crate::selection::BlockId, after_line: usize },
|
InlineResultDoubleClick { block_id: crate::selection::BlockId, after_line: usize },
|
||||||
|
/// mouse pressed on a heading or hr block, arms a free-layer drag.
|
||||||
|
BlockPromotePress(crate::selection::BlockId),
|
||||||
|
/// mouse pressed on an inline image, arms a free-layer drag.
|
||||||
|
ImagePromotePress { block_id: crate::selection::BlockId, after_line: usize, src: String },
|
||||||
|
/// mouse released after a block or image promote press.
|
||||||
|
PromoteRelease,
|
||||||
ToggleMenu(MenuCategory),
|
ToggleMenu(MenuCategory),
|
||||||
CloseMenu,
|
CloseMenu,
|
||||||
Shell(ShellAction),
|
Shell(ShellAction),
|
||||||
|
|
@ -312,6 +318,36 @@ enum LayerItem<'a> {
|
||||||
Image(&'a ComputedImage),
|
Image(&'a ComputedImage),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum FreeNodeId {
|
||||||
|
Block(crate::selection::BlockId),
|
||||||
|
Table(crate::selection::BlockId, usize),
|
||||||
|
Tree(crate::selection::BlockId, usize),
|
||||||
|
Image(crate::selection::BlockId, usize, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// position and size of a free-layer object in editor-content coordinates.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct FreePlacement {
|
||||||
|
pub layer: u8,
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub w: f32,
|
||||||
|
pub h: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pending drag state for promoting a block onto a free layer.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PromoteDragState {
|
||||||
|
pub node_id: FreeNodeId,
|
||||||
|
pub start_cursor: Point,
|
||||||
|
pub origin: (f32, f32),
|
||||||
|
pub size: (f32, f32),
|
||||||
|
pub layer: u8,
|
||||||
|
pub escalated: bool,
|
||||||
|
pub fallback_table_idx: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
impl LayerItem<'_> {
|
impl LayerItem<'_> {
|
||||||
fn element_height(&self, line_h: f32, font_size: f32) -> f32 {
|
fn element_height(&self, line_h: f32, font_size: f32) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -437,6 +473,11 @@ pub struct EditorState {
|
||||||
pub pending_shell_action: Option<ShellAction>,
|
pub pending_shell_action: Option<ShellAction>,
|
||||||
pub settings_open: bool,
|
pub settings_open: bool,
|
||||||
pub settings_view: SettingsView,
|
pub settings_view: SettingsView,
|
||||||
|
|
||||||
|
pub free_placements: HashMap<FreeNodeId, FreePlacement>,
|
||||||
|
pub frozen_doc_size: Option<(f32, f32)>,
|
||||||
|
pub viewport_size: (f32, f32),
|
||||||
|
pub promote_drag: Option<PromoteDragState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -553,9 +594,77 @@ impl EditorState {
|
||||||
pending_shell_action: None,
|
pending_shell_action: None,
|
||||||
settings_open: false,
|
settings_open: false,
|
||||||
settings_view: SettingsView::default(),
|
settings_view: SettingsView::default(),
|
||||||
|
free_placements: HashMap::new(),
|
||||||
|
frozen_doc_size: None,
|
||||||
|
viewport_size: (0.0, 0.0),
|
||||||
|
promote_drag: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// applies the current cursor delta to any active promote drag.
|
||||||
|
pub fn tick_promote_drag(&mut self) -> bool {
|
||||||
|
let (node_id, layer, origin, size, dx, dy, was_escalated) = {
|
||||||
|
let Some(pd) = self.promote_drag.as_ref() else { return false };
|
||||||
|
let dx = self.cursor_pos.x - pd.start_cursor.x;
|
||||||
|
let dy = self.cursor_pos.y - pd.start_cursor.y;
|
||||||
|
(pd.node_id.clone(), pd.layer, pd.origin, pd.size, dx, dy, pd.escalated)
|
||||||
|
};
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let threshold_sq = 16.0;
|
||||||
|
if dist_sq < threshold_sq && !was_escalated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(pd) = self.promote_drag.as_mut() { pd.escalated = true; }
|
||||||
|
let placement = FreePlacement {
|
||||||
|
layer,
|
||||||
|
x: origin.0 + dx,
|
||||||
|
y: origin.1 + dy,
|
||||||
|
w: size.0,
|
||||||
|
h: size.1,
|
||||||
|
};
|
||||||
|
self.free_placements.insert(node_id, placement);
|
||||||
|
if self.frozen_doc_size.is_none() && self.viewport_size != (0.0, 0.0) {
|
||||||
|
self.frozen_doc_size = Some(self.viewport_size);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// arms a drag promotion for any free-layer node in live mode.
|
||||||
|
fn start_promote(&mut self, node_id: FreeNodeId, fallback_table_idx: Option<usize>) {
|
||||||
|
if !matches!(self.render_mode, RenderMode::Live) { return; }
|
||||||
|
let existing = self.free_placements.get(&node_id).copied();
|
||||||
|
let default_size = (360.0, 240.0);
|
||||||
|
let (origin, size, layer) = match existing {
|
||||||
|
Some(p) => ((p.x, p.y), (p.w, p.h), p.layer),
|
||||||
|
None => ((self.cursor_pos.x, self.cursor_pos.y), default_size, 1),
|
||||||
|
};
|
||||||
|
self.promote_drag = Some(PromoteDragState {
|
||||||
|
node_id,
|
||||||
|
start_cursor: self.cursor_pos,
|
||||||
|
origin,
|
||||||
|
size,
|
||||||
|
layer,
|
||||||
|
escalated: false,
|
||||||
|
fallback_table_idx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// arms a corner-drag promotion for the table at a layout index.
|
||||||
|
pub fn begin_promote_table_corner(&mut self, block_idx: usize) {
|
||||||
|
let Some(&block_id) = self.layout.get(block_idx) else { return };
|
||||||
|
self.start_promote(FreeNodeId::Block(block_id), Some(block_idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// arms a drag promotion for a heading or hr block.
|
||||||
|
pub fn begin_promote_block(&mut self, block_id: crate::selection::BlockId) {
|
||||||
|
self.start_promote(FreeNodeId::Block(block_id), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// arms a drag promotion for an inline image at a specific anchor.
|
||||||
|
pub fn begin_promote_image(&mut self, block_id: crate::selection::BlockId, after_line: usize, src: String) {
|
||||||
|
self.start_promote(FreeNodeId::Image(block_id, after_line, src), None);
|
||||||
|
}
|
||||||
|
|
||||||
/// returns the queued shell action and clears it
|
/// returns the queued shell action and clears it
|
||||||
pub fn take_pending_shell_action(&mut self) -> Option<ShellAction> {
|
pub fn take_pending_shell_action(&mut self) -> Option<ShellAction> {
|
||||||
self.pending_shell_action.take()
|
self.pending_shell_action.take()
|
||||||
|
|
@ -2841,6 +2950,23 @@ impl EditorState {
|
||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
}
|
}
|
||||||
Message::TableMsg(idx, tmsg) => {
|
Message::TableMsg(idx, tmsg) => {
|
||||||
|
match &tmsg {
|
||||||
|
TableMessage::PromoteCornerPress => {
|
||||||
|
self.begin_promote_table_corner(idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TableMessage::PromoteCornerRelease => {
|
||||||
|
if let Some(pd) = self.promote_drag.take() {
|
||||||
|
if !pd.escalated {
|
||||||
|
if let Some(fb_idx) = pd.fallback_table_idx {
|
||||||
|
self.update(Message::TableMsg(fb_idx, TableMessage::SelectAll));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
let structural = matches!(
|
let structural = matches!(
|
||||||
&tmsg,
|
&tmsg,
|
||||||
TableMessage::InsertRowAbove
|
TableMessage::InsertRowAbove
|
||||||
|
|
@ -3312,6 +3438,15 @@ impl EditorState {
|
||||||
self.inline_press = None;
|
self.inline_press = None;
|
||||||
self.handle_result_extract(block_id, after_line);
|
self.handle_result_extract(block_id, after_line);
|
||||||
}
|
}
|
||||||
|
Message::BlockPromotePress(block_id) => {
|
||||||
|
self.begin_promote_block(block_id);
|
||||||
|
}
|
||||||
|
Message::ImagePromotePress { block_id, after_line, src } => {
|
||||||
|
self.begin_promote_image(block_id, after_line, src);
|
||||||
|
}
|
||||||
|
Message::PromoteRelease => {
|
||||||
|
self.promote_drag = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3406,7 +3541,11 @@ impl EditorState {
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
self.view_blocks()
|
let editor = self.view_blocks();
|
||||||
|
match self.build_free_overlay() {
|
||||||
|
Some(overlay) => iced_widget::stack![editor, overlay].into(),
|
||||||
|
None => editor,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode_label = match self.render_mode {
|
let mode_label = match self.render_mode {
|
||||||
|
|
@ -3507,6 +3646,9 @@ impl EditorState {
|
||||||
|
|
||||||
let mut global_line = 0usize;
|
let mut global_line = 0usize;
|
||||||
for (bi, &block_id) in self.layout.iter().enumerate() {
|
for (bi, &block_id) in self.layout.iter().enumerate() {
|
||||||
|
if self.free_placements.contains_key(&FreeNodeId::Block(block_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let block = self.registry.get(&block_id).unwrap();
|
let block = self.registry.get(&block_id).unwrap();
|
||||||
let any = block.as_any();
|
let any = block.as_any();
|
||||||
|
|
||||||
|
|
@ -3659,14 +3801,14 @@ impl EditorState {
|
||||||
|
|
||||||
if let Some(hb) = any.downcast_ref::<HeadingBlock>() {
|
if let Some(hb) = any.downcast_ref::<HeadingBlock>() {
|
||||||
let layered = <HeadingBlock as BlockTrait<Message>>::view(hb, &ctx);
|
let layered = <HeadingBlock as BlockTrait<Message>>::view(hb, &ctx);
|
||||||
block_elements.push(layered.base);
|
block_elements.push(self.wrap_block_with_promote(layered.base, hb.id));
|
||||||
global_line += 1;
|
global_line += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hr) = any.downcast_ref::<HrBlock>() {
|
if let Some(hr) = any.downcast_ref::<HrBlock>() {
|
||||||
let layered = <HrBlock as BlockTrait<Message>>::view(hr, &ctx);
|
let layered = <HrBlock as BlockTrait<Message>>::view(hr, &ctx);
|
||||||
block_elements.push(layered.base);
|
block_elements.push(self.wrap_block_with_promote(layered.base, hr.id));
|
||||||
global_line += 1;
|
global_line += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -3830,16 +3972,22 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
for ct in &self.computed_tables {
|
for ct in &self.computed_tables {
|
||||||
if ct.anchor.block_id == block_id {
|
if ct.anchor.block_id == block_id {
|
||||||
|
let id = FreeNodeId::Table(ct.anchor.block_id, ct.anchor.after_line);
|
||||||
|
if self.free_placements.contains_key(&id) { continue; }
|
||||||
items.push((ct.anchor.after_line, LayerItem::Table(ct)));
|
items.push((ct.anchor.after_line, LayerItem::Table(ct)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for ct in &self.computed_trees {
|
for ct in &self.computed_trees {
|
||||||
if ct.anchor.block_id == block_id {
|
if ct.anchor.block_id == block_id {
|
||||||
|
let id = FreeNodeId::Tree(ct.anchor.block_id, ct.anchor.after_line);
|
||||||
|
if self.free_placements.contains_key(&id) { continue; }
|
||||||
items.push((ct.anchor.after_line, LayerItem::Tree(ct)));
|
items.push((ct.anchor.after_line, LayerItem::Tree(ct)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for img in &self.computed_images {
|
for img in &self.computed_images {
|
||||||
if img.anchor.block_id == block_id {
|
if img.anchor.block_id == block_id {
|
||||||
|
let id = FreeNodeId::Image(img.anchor.block_id, img.anchor.after_line, img.src.clone());
|
||||||
|
if self.free_placements.contains_key(&id) { continue; }
|
||||||
items.push((img.anchor.after_line, LayerItem::Image(img)));
|
items.push((img.anchor.after_line, LayerItem::Image(img)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3917,40 +4065,9 @@ impl EditorState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
LayerItem::Table(ct) => {
|
LayerItem::Table(ct) => {
|
||||||
let mut table_rows: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
let inner = self.build_computed_table_widget(ct);
|
||||||
for (ri, row) in ct.rows.iter().enumerate() {
|
|
||||||
let is_header = ri == 0;
|
|
||||||
let cells: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = row.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ci, cell)| {
|
|
||||||
let cw = ct.col_widths.get(ci).copied().unwrap_or(80.0);
|
|
||||||
let mut txt = iced_widget::text(cell)
|
|
||||||
.font(syntax::EDITOR_FONT)
|
|
||||||
.size(self.font_size)
|
|
||||||
.color(oklab::lighten_for_size(p.text, self.font_size));
|
|
||||||
if is_header {
|
|
||||||
txt = txt.font(Font { weight: iced_wgpu::core::font::Weight::Bold, ..syntax::EDITOR_FONT });
|
|
||||||
}
|
|
||||||
iced_widget::container(txt)
|
|
||||||
.width(Length::Fixed(cw))
|
|
||||||
.padding(Padding { top: 2.0, right: 8.0, bottom: 2.0, left: 8.0 })
|
|
||||||
.style(move |_theme: &Theme| {
|
|
||||||
let bg_alpha = if is_header { 0.12 } else { 0.06 };
|
|
||||||
container::Style {
|
|
||||||
background: Some(Background::Color(Color { a: bg_alpha, ..p.surface1 })),
|
|
||||||
border: Border { color: p.surface1, width: 0.5, radius: border::Radius::default() },
|
|
||||||
text_color: None,
|
|
||||||
shadow: Shadow::default(),
|
|
||||||
snap: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
table_rows.push(iced_widget::row(cells).into());
|
|
||||||
}
|
|
||||||
let el: Element<'a, Message, Theme, iced_wgpu::Renderer> =
|
let el: Element<'a, Message, Theme, iced_wgpu::Renderer> =
|
||||||
iced_widget::container(iced_widget::column(table_rows))
|
iced_widget::container(inner)
|
||||||
.padding(Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 40.0 })
|
.padding(Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 40.0 })
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
@ -3990,10 +4107,16 @@ impl EditorState {
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
let wrapped = self.wrap_image_with_promote(
|
||||||
|
el,
|
||||||
|
img.anchor.block_id,
|
||||||
|
img.anchor.after_line,
|
||||||
|
img.src.clone(),
|
||||||
|
);
|
||||||
anchored.push(AnchoredItem {
|
anchored.push(AnchoredItem {
|
||||||
after_line: *after_line,
|
after_line: *after_line,
|
||||||
height: item.element_height(lh, self.font_size),
|
height: item.element_height(lh, self.font_size),
|
||||||
element: el,
|
element: wrapped,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4002,6 +4125,206 @@ impl EditorState {
|
||||||
anchored
|
anchored
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// builds a column of cell rows from a computed table.
|
||||||
|
fn build_computed_table_widget<'a>(
|
||||||
|
&self,
|
||||||
|
ct: &'a ComputedTable,
|
||||||
|
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
||||||
|
let p = palette::current();
|
||||||
|
let mut table_rows: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||||
|
for (ri, row) in ct.rows.iter().enumerate() {
|
||||||
|
let is_header = ri == 0;
|
||||||
|
let cells: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = row.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ci, cell)| {
|
||||||
|
let cw = ct.col_widths.get(ci).copied().unwrap_or(80.0);
|
||||||
|
let mut txt = iced_widget::text(cell)
|
||||||
|
.font(syntax::EDITOR_FONT)
|
||||||
|
.size(self.font_size)
|
||||||
|
.color(oklab::lighten_for_size(p.text, self.font_size));
|
||||||
|
if is_header {
|
||||||
|
txt = txt.font(Font { weight: iced_wgpu::core::font::Weight::Bold, ..syntax::EDITOR_FONT });
|
||||||
|
}
|
||||||
|
iced_widget::container(txt)
|
||||||
|
.width(Length::Fixed(cw))
|
||||||
|
.padding(Padding { top: 2.0, right: 8.0, bottom: 2.0, left: 8.0 })
|
||||||
|
.style(move |_theme: &Theme| {
|
||||||
|
let bg_alpha = if is_header { 0.12 } else { 0.06 };
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color { a: bg_alpha, ..p.surface1 })),
|
||||||
|
border: Border { color: p.surface1, width: 0.5, radius: border::Radius::default() },
|
||||||
|
text_color: None,
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
table_rows.push(iced_widget::row(cells).into());
|
||||||
|
}
|
||||||
|
iced_widget::column(table_rows).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wraps a block element with the promote press/release mouse area in live mode.
|
||||||
|
fn wrap_block_with_promote<'a>(
|
||||||
|
&self,
|
||||||
|
elem: Element<'a, Message, Theme, iced_wgpu::Renderer>,
|
||||||
|
block_id: crate::selection::BlockId,
|
||||||
|
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
||||||
|
if !matches!(self.render_mode, RenderMode::Live) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
MouseArea::new(elem)
|
||||||
|
.on_press(Message::BlockPromotePress(block_id))
|
||||||
|
.on_release(Message::PromoteRelease)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wraps an inline image element with the promote press/release mouse area in live mode.
|
||||||
|
fn wrap_image_with_promote<'a>(
|
||||||
|
&self,
|
||||||
|
elem: Element<'a, Message, Theme, iced_wgpu::Renderer>,
|
||||||
|
block_id: crate::selection::BlockId,
|
||||||
|
after_line: usize,
|
||||||
|
src: String,
|
||||||
|
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
||||||
|
if !matches!(self.render_mode, RenderMode::Live) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
MouseArea::new(elem)
|
||||||
|
.on_press(Message::ImagePromotePress { block_id, after_line, src })
|
||||||
|
.on_release(Message::PromoteRelease)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// builds the overlay widget for a single block at the given layout index.
|
||||||
|
fn build_free_block_widget(
|
||||||
|
&self,
|
||||||
|
block_id: crate::selection::BlockId,
|
||||||
|
) -> Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> {
|
||||||
|
let bi = self.layout.iter().position(|id| *id == block_id)?;
|
||||||
|
let block = self.registry.get(&block_id)?;
|
||||||
|
let any = block.as_any();
|
||||||
|
if let Some(tab) = any.downcast_ref::<TableBlock>() {
|
||||||
|
let editing_cell = match self.editing.as_ref() {
|
||||||
|
Some(path) if path.block_id == tab.id => match &path.inner {
|
||||||
|
crate::selection::InnerPath::Cell { row, col } => Some((*row, *col)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let block_idx = bi;
|
||||||
|
return Some(table_block::table_view(
|
||||||
|
tab,
|
||||||
|
editing_cell,
|
||||||
|
self.font_size,
|
||||||
|
&self.computed_cells,
|
||||||
|
move |tmsg| Message::TableMsg(block_idx, tmsg),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let ctx: ViewCtx<'_, Message> = ViewCtx {
|
||||||
|
block_index: bi,
|
||||||
|
selection: &self.selection,
|
||||||
|
focus: self.focus.as_ref(),
|
||||||
|
editing: self.editing.as_ref(),
|
||||||
|
font_size: self.font_size,
|
||||||
|
is_dark: true,
|
||||||
|
on_text_action: |idx, action| Message::BlockAction(idx, action),
|
||||||
|
on_table_msg: |idx, tmsg| Message::TableMsg(idx, tmsg),
|
||||||
|
computed_cells: &self.computed_cells,
|
||||||
|
};
|
||||||
|
if let Some(hb) = any.downcast_ref::<HeadingBlock>() {
|
||||||
|
return Some(<HeadingBlock as BlockTrait<Message>>::view(hb, &ctx).base);
|
||||||
|
}
|
||||||
|
if let Some(hr) = any.downcast_ref::<HrBlock>() {
|
||||||
|
return Some(<HrBlock as BlockTrait<Message>>::view(hr, &ctx).base);
|
||||||
|
}
|
||||||
|
if let Some(tree) = any.downcast_ref::<TreeBlock>() {
|
||||||
|
return Some(<TreeBlock as BlockTrait<Message>>::view(tree, &ctx).base);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// stacks free-placed objects at absolute positions over the editor body.
|
||||||
|
fn build_free_overlay(&self) -> Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> {
|
||||||
|
if self.free_placements.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut placed: Vec<(&FreeNodeId, &FreePlacement)> = self.free_placements.iter().collect();
|
||||||
|
placed.sort_by_key(|(_, pl)| pl.layer);
|
||||||
|
|
||||||
|
let mut layers: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||||
|
for &(id, placement) in &placed {
|
||||||
|
let inner_opt: Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> = match id {
|
||||||
|
FreeNodeId::Table(block_id, after_line) => self
|
||||||
|
.computed_tables
|
||||||
|
.iter()
|
||||||
|
.find(|ct| ct.anchor.block_id == *block_id && ct.anchor.after_line == *after_line)
|
||||||
|
.map(|ct| self.build_computed_table_widget(ct)),
|
||||||
|
FreeNodeId::Block(block_id) => self
|
||||||
|
.build_free_block_widget(*block_id)
|
||||||
|
.map(|el| self.wrap_block_with_promote(el, *block_id)),
|
||||||
|
FreeNodeId::Image(block_id, after_line, src) => self
|
||||||
|
.computed_images
|
||||||
|
.iter()
|
||||||
|
.find(|img| img.anchor.block_id == *block_id
|
||||||
|
&& img.anchor.after_line == *after_line
|
||||||
|
&& img.src == *src)
|
||||||
|
.map(|img| {
|
||||||
|
let inner: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
||||||
|
if let Some(entry) = self.image_cache.get(&img.src) {
|
||||||
|
iced_widget::image(entry.handle.clone())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
let p = palette::current();
|
||||||
|
iced_widget::text(format!("[image: {}]", img.alt))
|
||||||
|
.font(syntax::EDITOR_FONT)
|
||||||
|
.size(self.font_size)
|
||||||
|
.color(p.overlay0)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
self.wrap_image_with_promote(
|
||||||
|
inner,
|
||||||
|
img.anchor.block_id,
|
||||||
|
img.anchor.after_line,
|
||||||
|
img.src.clone(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
FreeNodeId::Tree(_, _) => None,
|
||||||
|
};
|
||||||
|
let Some(inner) = inner_opt else { continue };
|
||||||
|
|
||||||
|
let sized = iced_widget::container(iced_widget::scrollable(inner))
|
||||||
|
.width(Length::Fixed(placement.w))
|
||||||
|
.height(Length::Fixed(placement.h))
|
||||||
|
.style(|_theme: &Theme| {
|
||||||
|
let p = palette::current();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(p.base)),
|
||||||
|
border: Border { color: p.surface1, width: 1.0, radius: 4.0.into() },
|
||||||
|
text_color: None,
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let positioned = iced_widget::container(sized)
|
||||||
|
.padding(Padding { top: placement.y, right: 0.0, bottom: 0.0, left: placement.x })
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
layers.push(positioned.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if layers.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(iced_widget::stack(layers).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// builds the context menu overlay for a right-clicked cell
|
/// builds the context menu overlay for a right-clicked cell
|
||||||
fn context_menu_view(
|
fn context_menu_view(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,11 @@ pub fn create_native(
|
||||||
height: f32,
|
height: f32,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
) -> Option<ViewportHandle> {
|
) -> Option<ViewportHandle> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
let backends = wgpu::Backends::METAL;
|
let backends = wgpu::Backends::METAL;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let backends = wgpu::Backends::DX12;
|
let backends = wgpu::Backends::DX12;
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
|
||||||
let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
|
let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
|
||||||
|
|
||||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
|
|
@ -147,6 +147,12 @@ pub fn create_native(
|
||||||
|
|
||||||
let caps = surface.get_capabilities(&adapter);
|
let caps = surface.get_capabilities(&adapter);
|
||||||
let format = caps.formats.first().copied()?;
|
let format = caps.formats.first().copied()?;
|
||||||
|
let alpha_mode = caps.alpha_modes.first().copied().unwrap_or(wgpu::CompositeAlphaMode::Auto);
|
||||||
|
crate::ios_dlog!(
|
||||||
|
"surface formats={:?} chose={:?} alpha_modes={:?} chose={:?} adapter={}",
|
||||||
|
caps.formats, format, caps.alpha_modes, alpha_mode,
|
||||||
|
adapter.get_info().name,
|
||||||
|
);
|
||||||
|
|
||||||
surface.configure(
|
surface.configure(
|
||||||
&device,
|
&device,
|
||||||
|
|
@ -156,11 +162,7 @@ pub fn create_native(
|
||||||
width: phys_w.max(1),
|
width: phys_w.max(1),
|
||||||
height: phys_h.max(1),
|
height: phys_h.max(1),
|
||||||
present_mode: wgpu::PresentMode::AutoVsync,
|
present_mode: wgpu::PresentMode::AutoVsync,
|
||||||
alpha_mode: caps
|
alpha_mode,
|
||||||
.alpha_modes
|
|
||||||
.first()
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
|
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 2,
|
||||||
},
|
},
|
||||||
|
|
@ -175,6 +177,11 @@ pub fn create_native(
|
||||||
Shell::headless(),
|
Shell::headless(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// NOTE: `Font::DEFAULT` is cosmic-text's hardcoded "Noto Sans Mono" which
|
||||||
|
// iOS doesn't ship — we override per-span with EDITOR_FONT, but anything
|
||||||
|
// unstyled falls back to this missing font. If iOS shows invisible text
|
||||||
|
// while spans render, this is the suspect.
|
||||||
|
crate::ios_dlog!("renderer init font=Font::DEFAULT (= cosmic 'Noto Sans Mono', iOS likely missing) editor_font={:?}", crate::syntax::EDITOR_FONT);
|
||||||
let renderer = iced_wgpu::Renderer::new(engine, Font::DEFAULT, Pixels(16.0));
|
let renderer = iced_wgpu::Renderer::new(engine, Font::DEFAULT, Pixels(16.0));
|
||||||
|
|
||||||
let viewport =
|
let viewport =
|
||||||
|
|
@ -743,6 +750,9 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
// anchor the context menu at the current position in the same frame.
|
// anchor the context menu at the current position in the same frame.
|
||||||
if let Some(pt) = handle.cursor.position() {
|
if let Some(pt) = handle.cursor.position() {
|
||||||
handle.state.cursor_pos = pt;
|
handle.state.cursor_pos = pt;
|
||||||
|
if handle.state.tick_promote_drag() {
|
||||||
|
handle.needs_redraw = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handle.state.sync_focused_cell(focused_id.as_ref());
|
handle.state.sync_focused_cell(focused_id.as_ref());
|
||||||
|
|
||||||
|
|
@ -771,6 +781,26 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// First-frame palette/theme dump so we can spot a "white text on white
|
||||||
|
// background" misconfiguration. Then a periodic re-log so palette swaps
|
||||||
|
// mid-session show up too.
|
||||||
|
{
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
static FRAME: AtomicU64 = AtomicU64::new(0);
|
||||||
|
let n = FRAME.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if n == 0 || n == 60 || n % 600 == 0 {
|
||||||
|
let p = palette::current();
|
||||||
|
let _ = (n, &p, &theme, &style);
|
||||||
|
crate::ios_dlog!(
|
||||||
|
"render frame={n} theme=Dark style.text={:?} palette.base={:?} palette.text={:?} palette.surface0={:?}",
|
||||||
|
style.text_color,
|
||||||
|
p.base,
|
||||||
|
p.text,
|
||||||
|
p.surface0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut ui = UserInterface::build(
|
let mut ui = UserInterface::build(
|
||||||
handle.state.view(),
|
handle.state.view(),
|
||||||
Size::new(logical_size.width, logical_size.height),
|
Size::new(logical_size.width, logical_size.height),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,20 @@
|
||||||
|
|
||||||
use std::ffi::{c_char, c_void, CStr, CString};
|
use std::ffi::{c_char, c_void, CStr, CString};
|
||||||
|
|
||||||
|
/// Gated diagnostic logging — `eprintln!` to stderr, prefixed with the
|
||||||
|
/// callsite. Compiles to nothing outside debug builds on iOS.
|
||||||
|
/// AcordApp.swift's captureStderr() pumps stderr → stdout + NSLog so output
|
||||||
|
/// shows up under `cargo xtask debug-ios --console`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! ios_dlog {
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
#[cfg(all(debug_assertions, target_os = "ios"))]
|
||||||
|
{
|
||||||
|
eprintln!("[viewport {}:{}] {}", file!(), line!(), format!($($arg)*));
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod blocks;
|
pub mod blocks;
|
||||||
pub mod bridge;
|
pub mod bridge;
|
||||||
|
|
@ -136,12 +150,20 @@ pub extern "C" fn viewport_create(
|
||||||
scale: f32,
|
scale: f32,
|
||||||
) -> *mut ViewportHandle {
|
) -> *mut ViewportHandle {
|
||||||
install_panic_hook();
|
install_panic_hook();
|
||||||
|
ios_dlog!("viewport_create: w={width} h={height} scale={scale} font={:?}", crate::syntax::EDITOR_FONT);
|
||||||
if nsview.is_null() {
|
if nsview.is_null() {
|
||||||
|
ios_dlog!("viewport_create: nsview NULL — bailing");
|
||||||
return std::ptr::null_mut();
|
return std::ptr::null_mut();
|
||||||
}
|
}
|
||||||
match handle::create(nsview, width, height, scale) {
|
match handle::create(nsview, width, height, scale) {
|
||||||
Some(h) => Box::into_raw(Box::new(h)),
|
Some(h) => {
|
||||||
None => std::ptr::null_mut(),
|
ios_dlog!("viewport_create: ok — surface ready");
|
||||||
|
Box::into_raw(Box::new(h))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ios_dlog!("viewport_create: handle::create returned None");
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,6 +198,7 @@ pub extern "C" fn viewport_resize(
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
handle::resize(h, width, height, scale);
|
handle::resize(h, width, height, scale);
|
||||||
|
h.state.viewport_size = (width, height);
|
||||||
h.needs_redraw = true;
|
h.needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@ const MD_BOLD_ITALIC: u8 = 44;
|
||||||
/// cosmic-text / fontdb to resolve real Bold, Italic and BoldItalic faces,
|
/// cosmic-text / fontdb to resolve real Bold, Italic and BoldItalic faces,
|
||||||
/// which the generic monospace fallback does not reliably do on macOS because
|
/// which the generic monospace fallback does not reliably do on macOS because
|
||||||
/// cosmic-text hardcodes its default monospace family to "Noto Sans Mono".
|
/// cosmic-text hardcodes its default monospace family to "Noto Sans Mono".
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
pub const EDITOR_FONT: Font = Font::with_name("Menlo");
|
pub const EDITOR_FONT: Font = Font::with_name("Menlo");
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub const EDITOR_FONT: Font = Font::with_name("Consolas");
|
pub const EDITOR_FONT: Font = Font::with_name("Consolas");
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
|
||||||
pub const EDITOR_FONT: Font = Font::with_name("DejaVu Sans Mono");
|
pub const EDITOR_FONT: Font = Font::with_name("DejaVu Sans Mono");
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,10 @@ pub enum TableMessage {
|
||||||
/// Click on a column-header sort arrow: cycles that column through
|
/// Click on a column-header sort arrow: cycles that column through
|
||||||
/// Neutral → Asc → Desc → Neutral and re-applies the composite sort.
|
/// Neutral → Asc → Desc → Neutral and re-applies the composite sort.
|
||||||
CycleSort(usize),
|
CycleSort(usize),
|
||||||
|
/// mouse pressed on the corner select-all marker, arms a free-layer drag.
|
||||||
|
PromoteCornerPress,
|
||||||
|
/// mouse released on the corner select-all marker, ends or cancels the drag.
|
||||||
|
PromoteCornerRelease,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait-implementing block for tables. Owns all the per-table mutable state
|
/// Trait-implementing block for tables. Owns all the per-table mutable state
|
||||||
|
|
@ -852,6 +856,7 @@ impl TableBlock {
|
||||||
// so we just clear the bookkeeping.
|
// so we just clear the bookkeeping.
|
||||||
self.end_drag_select();
|
self.end_drag_select();
|
||||||
}
|
}
|
||||||
|
TableMessage::PromoteCornerPress | TableMessage::PromoteCornerRelease => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1254,7 +1259,7 @@ where
|
||||||
// Visible whenever the chrome is active so the user always has a
|
// Visible whenever the chrome is active so the user always has a
|
||||||
// reachable affordance once they've touched the table once.
|
// reachable affordance once they've touched the table once.
|
||||||
let corner: Element<'a, Message, Theme, iced_wgpu::Renderer> = if chrome_active {
|
let corner: Element<'a, Message, Theme, iced_wgpu::Renderer> = if chrome_active {
|
||||||
iced_widget::button(
|
let inner = container(
|
||||||
text("\u{25A0}")
|
text("\u{25A0}")
|
||||||
.size(corner_font)
|
.size(corner_font)
|
||||||
.font(EDITOR_FONT)
|
.font(EDITOR_FONT)
|
||||||
|
|
@ -1262,9 +1267,20 @@ where
|
||||||
.width(Length::Fixed(ROW_NUMBER_WIDTH))
|
.width(Length::Fixed(ROW_NUMBER_WIDTH))
|
||||||
.height(Length::Fixed(header_h))
|
.height(Length::Fixed(header_h))
|
||||||
.padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0 })
|
.padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0 })
|
||||||
.style(plus_button_style)
|
.style(|_theme: &Theme| {
|
||||||
.on_press(on_msg(TableMessage::SelectAll))
|
let p = palette::current();
|
||||||
.into()
|
container::Style {
|
||||||
|
background: None,
|
||||||
|
border: Border::default(),
|
||||||
|
text_color: Some(p.overlay0),
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MouseArea::new(inner)
|
||||||
|
.on_press(on_msg(TableMessage::PromoteCornerPress))
|
||||||
|
.on_release(on_msg(TableMessage::PromoteCornerRelease))
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
container(text(""))
|
container(text(""))
|
||||||
.width(Length::Fixed(ROW_NUMBER_WIDTH))
|
.width(Length::Fixed(ROW_NUMBER_WIDTH))
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Command, ExitCode};
|
use std::process::{Command, ExitCode};
|
||||||
|
|
||||||
const KNOWN_PLATFORMS: &[&str] = &["macos", "windows", "linux", "ios"];
|
const KNOWN_PLATFORMS: &[&str] = &["macos", "windows", "linux", "ios", "android"];
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let args: Vec<String> = env::args().skip(1).collect();
|
let args: Vec<String> = env::args().skip(1).collect();
|
||||||
|
|
@ -111,11 +111,15 @@ fn print_help() {
|
||||||
eprintln!(" --all all six targets");
|
eprintln!(" --all all six targets");
|
||||||
eprintln!(" --target <name> e.g. macos-aarch64, windows-x86_64");
|
eprintln!(" --target <name> e.g. macos-aarch64, windows-x86_64");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("append -macos / -windows / -linux / -ios to any command to force a platform.");
|
eprintln!("append -macos / -windows / -linux / -ios / -android to any command to force a platform.");
|
||||||
eprintln!(" e.g. cargo xtask build-universal-macos");
|
eprintln!(" e.g. cargo xtask build-universal-macos");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("ios:");
|
eprintln!("ios:");
|
||||||
|
eprintln!(" cargo xtask select-ios interactively pick the iPad / simulator to target");
|
||||||
eprintln!(" cargo xtask build-ios build the .app bundle for the iPad simulator");
|
eprintln!(" cargo xtask build-ios build the .app bundle for the iPad simulator");
|
||||||
eprintln!(" cargo xtask install-ios build + install + launch (paired device wins, else sim)");
|
eprintln!(" cargo xtask install-ios build + install + launch (uses selected target)");
|
||||||
eprintln!(" cargo xtask xcodeproj-ios generate Acord.xcodeproj for finishing in Xcode");
|
eprintln!(" cargo xtask xcodeproj-ios generate Acord.xcodeproj for finishing in Xcode");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("android:");
|
||||||
|
eprintln!(" cargo xtask select-android interactively pick the Android device / emulator (stub)");
|
||||||
}
|
}
|
||||||
|
|
|
||||||