Switching to node based design. See NodeItems project

This commit is contained in:
Casey Fleser 2023-03-07 06:41:47 -06:00
parent 3f0bbad3a3
commit f6d596ad1d
37 changed files with 868 additions and 600 deletions

View file

@ -13,7 +13,6 @@
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */; };
C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4B2861E37900C2EF35 /* AppHeader.swift */; };
C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4D2861E4E400C2EF35 /* AppContent.swift */; };
C90BCE502861E9D000C2EF35 /* SourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4F2861E9D000C2EF35 /* SourceState.swift */; };
C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */; };
C90DCC142896AAAA0072E403 /* ContentHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90DCC132896AAAA0072E403 /* ContentHeader.swift */; };
C90DCC162896B0370072E403 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90DCC152896B0370072E403 /* AppearancePicker.swift */; };
@ -21,6 +20,19 @@
C927A0DB2846502300533D66 /* PathActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0DA2846502300533D66 /* PathActions.swift */; };
C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */; };
C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */; };
C966876C29B7641F007BB3F5 /* FilteredNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966875E29B7641F007BB3F5 /* FilteredNode.swift */; };
C966876D29B7641F007BB3F5 /* SimApp+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876029B7641F007BB3F5 /* SimApp+Node.swift */; };
C966876E29B7641F007BB3F5 /* SimProductFamily+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */; };
C966876F29B7641F007BB3F5 /* SimPlatform+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */; };
C966877029B7641F007BB3F5 /* SimDeviceType+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */; };
C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */; };
C966877229B7641F007BB3F5 /* SimDevice+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876529B7641F007BB3F5 /* SimDevice+Node.swift */; };
C966877329B7641F007BB3F5 /* NodeListBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876629B7641F007BB3F5 /* NodeListBuilder.swift */; };
C966877429B7641F007BB3F5 /* NodeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876829B7641F007BB3F5 /* NodeLabel.swift */; };
C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876929B7641F007BB3F5 /* FilteredNodeView.swift */; };
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876A29B7641F007BB3F5 /* NodeView.swift */; };
C966877729B7641F007BB3F5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876B29B7641F007BB3F5 /* Node.swift */; };
C966877829B76533007BB3F5 /* SourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4F2861E9D000C2EF35 /* SourceState.swift */; };
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; };
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; };
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; };
@ -35,13 +47,6 @@
C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F87A283E40C800D491F4 /* SimCtl.swift */; };
C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */; };
C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */; };
C9D73C25285C8C0C0044A279 /* SourceItemData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C24285C8C0C0044A279 /* SourceItemData.swift */; };
C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C28285C8C4B0044A279 /* SourceItem.swift */; };
C9DD54C32860936D00D46AB3 /* SourceItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */; };
C9DD54C52860938C00D46AB3 /* SourceItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */; };
C9DD54C7286093A500D46AB3 /* SourceItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */; };
C9DD54C9286093C100D46AB3 /* SourceItemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */; };
C9DD54CB2860948600D46AB3 /* SourceItemLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */; };
C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */; };
C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */; };
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */; };
@ -64,6 +69,18 @@
C927A0DA2846502300533D66 /* PathActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathActions.swift; sourceTree = "<group>"; };
C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceButtonStyle.swift; sourceTree = "<group>"; };
C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemIconButtonStyle.swift; sourceTree = "<group>"; };
C966875E29B7641F007BB3F5 /* FilteredNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilteredNode.swift; sourceTree = "<group>"; };
C966876029B7641F007BB3F5 /* SimApp+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimApp+Node.swift"; sourceTree = "<group>"; };
C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimProductFamily+Node.swift"; sourceTree = "<group>"; };
C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimPlatform+Node.swift"; sourceTree = "<group>"; };
C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimDeviceType+Node.swift"; sourceTree = "<group>"; };
C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimRuntime+Node.swift"; sourceTree = "<group>"; };
C966876529B7641F007BB3F5 /* SimDevice+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimDevice+Node.swift"; sourceTree = "<group>"; };
C966876629B7641F007BB3F5 /* NodeListBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeListBuilder.swift; sourceTree = "<group>"; };
C966876829B7641F007BB3F5 /* NodeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeLabel.swift; sourceTree = "<group>"; };
C966876929B7641F007BB3F5 /* FilteredNodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilteredNodeView.swift; sourceTree = "<group>"; };
C966876A29B7641F007BB3F5 /* NodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeView.swift; sourceTree = "<group>"; };
C966876B29B7641F007BB3F5 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = "<group>"; };
C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; };
C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = "<group>"; };
@ -80,13 +97,6 @@
C982F87A283E40C800D491F4 /* SimCtl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimCtl.swift; sourceTree = "<group>"; };
C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggle.swift; sourceTree = "<group>"; };
C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggleStyle.swift; sourceTree = "<group>"; };
C9D73C24285C8C0C0044A279 /* SourceItemData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemData.swift; sourceTree = "<group>"; };
C9D73C28285C8C4B0044A279 /* SourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItem.swift; sourceTree = "<group>"; };
C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemGroup.swift; sourceTree = "<group>"; };
C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemLink.swift; sourceTree = "<group>"; };
C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemContent.swift; sourceTree = "<group>"; };
C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemImage.swift; sourceTree = "<group>"; };
C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemLabel.swift; sourceTree = "<group>"; };
C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeContent.swift; sourceTree = "<group>"; };
C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeHeader.swift; sourceTree = "<group>"; };
C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeContent.swift; sourceTree = "<group>"; };
@ -115,13 +125,50 @@
path = Styles;
sourceTree = "<group>";
};
C966875D29B7641F007BB3F5 /* Node */ = {
isa = PBXGroup;
children = (
C966875F29B7641F007BB3F5 /* Conforming */,
C966876729B7641F007BB3F5 /* Views */,
C966875E29B7641F007BB3F5 /* FilteredNode.swift */,
C966876629B7641F007BB3F5 /* NodeListBuilder.swift */,
C966876B29B7641F007BB3F5 /* Node.swift */,
);
path = Node;
sourceTree = "<group>";
};
C966875F29B7641F007BB3F5 /* Conforming */ = {
isa = PBXGroup;
children = (
C966876029B7641F007BB3F5 /* SimApp+Node.swift */,
C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */,
C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */,
C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */,
C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */,
C966876529B7641F007BB3F5 /* SimDevice+Node.swift */,
);
path = Conforming;
sourceTree = "<group>";
};
C966876729B7641F007BB3F5 /* Views */ = {
isa = PBXGroup;
children = (
C966876829B7641F007BB3F5 /* NodeLabel.swift */,
C966876929B7641F007BB3F5 /* FilteredNodeView.swift */,
C966876A29B7641F007BB3F5 /* NodeView.swift */,
);
path = Views;
sourceTree = "<group>";
};
C982F84C283B9F9000D491F4 = {
isa = PBXGroup;
children = (
C982F857283B9F9000D491F4 /* SimDirs */,
C982F856283B9F9000D491F4 /* Products */,
);
indentWidth = 4;
sourceTree = "<group>";
tabWidth = 4;
};
C982F856283B9F9000D491F4 /* Products */ = {
isa = PBXGroup;
@ -138,6 +185,7 @@
C982F85A283B9F9000D491F4 /* ContentView.swift */,
C927A0D82846414900533D66 /* Helpers.swift */,
C982F867283BA09B00D491F4 /* Model */,
C966875D29B7641F007BB3F5 /* Node */,
C9D73C23285C8B3B0044A279 /* Presentation */,
C982F881283E7F0400D491F4 /* Views */,
C982F85C283B9F9200D491F4 /* Assets.xcassets */,
@ -174,7 +222,6 @@
isa = PBXGroup;
children = (
C95CC0F628B240F500928FAE /* Styles */,
C9DD54C12860935300D46AB3 /* SourceItem Views */,
C9DD54CC2860992200D46AB3 /* Model Views */,
C90DCC152896B0370072E403 /* AppearancePicker.swift */,
C90DCC132896AAAA0072E403 /* ContentHeader.swift */,
@ -190,26 +237,12 @@
C9D73C23285C8B3B0044A279 /* Presentation */ = {
isa = PBXGroup;
children = (
C9D73C28285C8C4B0044A279 /* SourceItem.swift */,
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */,
C9D73C24285C8C0C0044A279 /* SourceItemData.swift */,
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */,
);
path = Presentation;
sourceTree = "<group>";
};
C9DD54C12860935300D46AB3 /* SourceItem Views */ = {
isa = PBXGroup;
children = (
C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */,
C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */,
C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */,
C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */,
C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */,
);
path = "SourceItem Views";
sourceTree = "<group>";
};
C9DD54CC2860992200D46AB3 /* Model Views */ = {
isa = PBXGroup;
children = (
@ -295,30 +328,35 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C9DD54C9286093C100D46AB3 /* SourceItemImage.swift in Sources */,
C9D73C25285C8C0C0044A279 /* SourceItemData.swift in Sources */,
C966877729B7641F007BB3F5 /* Node.swift in Sources */,
C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */,
C966877229B7641F007BB3F5 /* SimDevice+Node.swift in Sources */,
C966876C29B7641F007BB3F5 /* FilteredNode.swift in Sources */,
C966877029B7641F007BB3F5 /* SimDeviceType+Node.swift in Sources */,
C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */,
C927A0DB2846502300533D66 /* PathActions.swift in Sources */,
C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */,
C966876F29B7641F007BB3F5 /* SimPlatform+Node.swift in Sources */,
C90DCC142896AAAA0072E403 /* ContentHeader.swift in Sources */,
C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */,
C90BCE502861E9D000C2EF35 /* SourceState.swift in Sources */,
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */,
C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */,
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */,
C966877829B76533007BB3F5 /* SourceState.swift in Sources */,
C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */,
C9DD54C7286093A500D46AB3 /* SourceItemContent.swift in Sources */,
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */,
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */,
C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */,
C90DCC162896B0370072E403 /* AppearancePicker.swift in Sources */,
C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */,
C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */,
C966877429B7641F007BB3F5 /* NodeLabel.swift in Sources */,
C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */,
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */,
C982F875283CEEBB00D491F4 /* SimDevice.swift in Sources */,
C9DD54C52860938C00D46AB3 /* SourceItemLink.swift in Sources */,
C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */,
C966876D29B7641F007BB3F5 /* SimApp+Node.swift in Sources */,
C982F873283CE9AD00D491F4 /* SimDeviceType.swift in Sources */,
C9DD54CB2860948600D46AB3 /* SourceItemLabel.swift in Sources */,
C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */,
C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */,
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */,
@ -328,10 +366,10 @@
C982F86B283BA22100D491F4 /* SimPlatform.swift in Sources */,
C927A0D92846414900533D66 /* Helpers.swift in Sources */,
C90BCE462861D57100C2EF35 /* DeviceHeader.swift in Sources */,
C9DD54C32860936D00D46AB3 /* SourceItemGroup.swift in Sources */,
C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */,
C966877329B7641F007BB3F5 /* NodeListBuilder.swift in Sources */,
C982F879283D042E00D491F4 /* SimModel.swift in Sources */,
C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */,
C966876E29B7641F007BB3F5 /* SimProductFamily+Node.swift in Sources */,
C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -8,7 +8,8 @@
import SwiftUI
struct ContentView: View {
@ObservedObject var state : SourceState
@ObservedObject var state : SourceState
@State var filter = SourceFilter.restore()
init(model: SimModel) {
state = SourceState(model: model)
@ -17,32 +18,13 @@ struct ContentView: View {
var body: some View {
VStack {
NavigationView {
List(selection: $state.selection) {
Divider()
switch state.base {
case .placeholder:
Text("Placeholder")
case let .device(_, item):
ForEach(item.visibleChildren) {
SourceItemGroup(item: $0, selection: $state.selection)
}
case let .runtime(_, item):
ForEach(item.visibleChildren) {
SourceItemGroup(item: $0, selection: $state.selection)
}
}
}
.toolbar {
ToolbarItem { ToolbarMenu(state: state) }
}
.frame(minWidth: 200)
FilteredNodeView(filter: $filter) { state.items }
.id(state.style)
.toolbar { ToolbarItem { ToolbarMenu(state: state, filter: $filter) } }
.frame(minWidth: 200)
Image("Icon-256") // Initial View
}
.searchable(text: $state.filter.searchTerm, placement: .sidebar)
}
}
}

View file

@ -118,11 +118,3 @@ extension SimApp {
var isOn : Bool { self == .launched }
}
}
extension SimApp: SourceItemData {
var title : String { return displayName }
var headerTitle : String { "App: \(title)" }
var imageDesc : SourceImageDesc { nsIcon.map { .icon(nsImage: $0) } ?? .symbol(systemName: "questionmark.app.dashed") }
var optionTrait : SourceFilter.Options { .withApps }
}

View file

@ -40,6 +40,7 @@ class SimDevice: ObservableObject, Decodable {
let dataPathSize : Int
let logPath : String
let deviceTypeIdentifier : String
var deviceType : SimDeviceType?
var deviceModel : String?
var apps = [SimApp]()
var dataURL : URL { URL(fileURLWithPath: dataPath) }
@ -313,14 +314,17 @@ extension SimDevice {
}
}
extension SimDevice: SourceItemData {
var title : String { return name }
var headerTitle : String { "Device: \(title)" }
var isEnabled : Bool { isBooted }
var imageDesc : SourceImageDesc { .symbol(systemName: "questionmark.circle", color: isAvailable ? .green : .red) }
}
extension Array where Element == SimDevice {
func linkingDeviceType(_ deviceType: SimDeviceType) -> Self {
let devices = filter { $0.isDeviceOfType(deviceType) }
for device in devices {
device.deviceType = deviceType
}
return devices
}
func of(deviceType: SimDeviceType) -> Self {
filter { $0.isDeviceOfType(deviceType) }
}

View file

@ -39,12 +39,6 @@ struct SimDeviceType: Decodable {
}
}
extension SimDeviceType: SourceItemData {
var title : String { return name }
var headerTitle : String { "Device Type: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: productFamily.symbolName) }
}
extension Array where Element == SimDeviceType {
func supporting(productFamily: SimProductFamily) -> Self {
filter { $0.supports(productFamily: productFamily) }

View file

@ -22,9 +22,3 @@ enum SimPlatform: String, Decodable {
}
}
}
extension SimPlatform: SourceItemData {
var title : String { self.rawValue }
var headerTitle : String { "Platform: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: symbolName) }
}

View file

@ -24,9 +24,3 @@ enum SimProductFamily: String, Decodable {
}
}
}
extension SimProductFamily: SourceItemData {
var title : String { self.rawValue }
var headerTitle : String { "Product Family: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: symbolName) }
}

View file

@ -114,14 +114,6 @@ class SimRuntime: ObservableObject, Comparable, Decodable {
}
}
extension SimRuntime: SourceItemData {
var title : String { return name }
var headerTitle : String { "Runtime: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: "shippingbox", color: isAvailable ? .green : .red) }
var optionTrait : SourceFilter.Options { isAvailable ? .runtimeInstalled : [] }
}
extension Array where Element == SimRuntime {
mutating func indexOfMatchedOrCreated(identifier: String) throws -> Index {
return try firstIndex { $0.identifier == identifier } ?? {

View file

@ -0,0 +1,33 @@
//
// SimApp+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimApp: Node {
var title : String { return displayName }
var headerTitle : String { "App: \(title)" }
var header : some View { AppHeader(app: self) }
var content : some View { AppContent(app: self) }
func icon(forHeader: Bool) -> some View {
if let nsIcon = nsIcon {
let iconSize : CGFloat = forHeader ? 128 : 20
Image(nsImage: nsIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(iconSize / 5.0)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
.frame(maxWidth: iconSize, maxHeight: iconSize)
}
else {
symbolIcon("questionmark.app.dashed", forHeader: forHeader)
}
}
}

View file

@ -0,0 +1,31 @@
//
// SimDevice+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimDevice: Node {
var title : String { return name }
var headerTitle : String { "Device: \(title)" }
var header : some View { DeviceHeader(device: self) }
var content : some View { DeviceContent(self) }
// var isEnabled : Bool { isBooted }
var iconName : String { deviceType?.productFamily.symbolName ?? "questionmark.circle" }
var items : [SimApp]? {
get { apps }
set { apps = newValue ?? [] }
}
func icon(forHeader: Bool) -> some View {
symbolIcon(iconName, color: isAvailable ? .green : .red, forHeader: forHeader)
}
func matchedFilterOptions() -> SourceFilter.Options {
return !apps.isEmpty ? .withApps : []
}
}

View file

@ -0,0 +1,20 @@
//
// SimDeviceType+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimDeviceType: Node {
var title : String { return name }
var headerTitle : String { "Device Type: \(title)" }
var header : some View { DeviceTypeHeader(deviceType: self) }
var content : some View { DeviceTypeContent(deviceType: self) }
func icon(forHeader: Bool) -> some View {
symbolIcon(productFamily.symbolName, forHeader: forHeader)
}
}

View file

@ -0,0 +1,21 @@
//
// SimPlatform+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimPlatform: Node {
var title : String { self.rawValue }
var headerTitle : String { "Platform: \(title)" }
var header : some View { get { EmptyView() } }
var content : some View { get { EmptyView() } }
func icon(forHeader: Bool) -> some View {
symbolIcon(symbolName, forHeader: forHeader)
}
}

View file

@ -0,0 +1,21 @@
//
// SimProductFamily+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimProductFamily: Node {
var title : String { self.rawValue }
var headerTitle : String { "Product Family: \(title)" }
var header : some View { get { EmptyView() } }
var content : some View { get { EmptyView() } }
func icon(forHeader: Bool) -> some View {
symbolIcon(symbolName, forHeader: forHeader)
}
}

View file

@ -0,0 +1,24 @@
//
// SimRuntime+Node.swift
// SimDirs
//
// Created by Casey Fleser on 3/5/23.
//
import SwiftUI
extension SimRuntime: Node {
var title : String { return name }
var headerTitle : String { "Runtime: \(title)" }
var header : some View { RuntimeHeader(runtime: self) }
var content : some View { RuntimeContent(runtime: self) }
func icon(forHeader: Bool) -> some View {
symbolIcon("shippingbox", color: isAvailable ? .green : .red, forHeader: forHeader)
}
func matchedFilterOptions() -> SourceFilter.Options {
return isAvailable ? .runtimeInstalled : []
}
}

View file

@ -0,0 +1,73 @@
//
// FilteredNode.swift
// NodeItems
//
// Created by Casey Fleser on 3/3/23.
//
import SwiftUI
import Combine
class FilteredNode<T: Node>: Node, ObservableObject {
typealias FilteredList = [FilteredNode<T.List.Element>]
@Published var filtered : Bool
@Published var isExpanded = false
var wrappedNode : T
var title : String { wrappedNode.title }
var headerTitle : String { wrappedNode.headerTitle }
var header : some View { wrappedNode.header }
var content : some View { wrappedNode.content }
var items : FilteredList?
var children : FilteredList { items ?? [] }
init(_ node: T) {
self.wrappedNode = node
self.filtered = false
self.items = node.items?.asFilteredNodes()
}
func icon(forHeader: Bool) -> some View {
wrappedNode.icon(forHeader: forHeader)
}
func toggleExpanded(_ expanded: Bool? = nil, deep: Bool) {
isExpanded = expanded ?? !isExpanded
if deep {
for child in children {
child.toggleExpanded(isExpanded, deep: true)
}
}
}
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool {
wrappedNode.matchesFilter(filter, inherited: options)
}
func matchedFilterOptions() -> SourceFilter.Options {
return wrappedNode.matchedFilterOptions()
}
@discardableResult
func applyFilter(_ filter: SourceFilter, inheriting options: SourceFilter.Options = []) -> Bool {
let updatedOptions = options.union(matchedFilterOptions())
let childMatch = children.reduce(false) { result, node in
node.applyFilter(filter, inheriting: updatedOptions) || result // deliberately not short circuiting here
}
let nodeMatch = childMatch || wrappedNode.matchesFilter(filter, inherited: updatedOptions)
filtered = !nodeMatch
return nodeMatch
}
}
extension NodeList {
func asFilteredNodes() -> [FilteredNode<Element>] {
self.map { FilteredNode($0) }
}
}

158
SimDirs/Node/Node.swift Normal file
View file

@ -0,0 +1,158 @@
//
// Node.swift
// NodeItems
//
// Created by Casey Fleser on 3/2/23.
//
import SwiftUI
protocol Node: NodeSource {
associatedtype Icon: View
associatedtype Header: View
associatedtype Content: View
var title : String { get }
var headerTitle : String { get }
@ViewBuilder var header : Header { get }
@ViewBuilder var content : Content { get }
@ViewBuilder
func icon(forHeader: Bool) -> Icon
func matchedFilterOptions() -> SourceFilter.Options
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool
}
extension Node {
var items : [LeafNode]? {
get { nil }
set { }
}
@ViewBuilder
func symbolIcon(_ systemName: String, color: Color? = nil, forHeader: Bool) -> some View {
if forHeader {
Image(systemName: systemName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 128, maxHeight: 128)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
}
else {
Image(systemName: systemName)
.foregroundColor(color ?? .primary)
.symbolRenderingMode(.hierarchical)
}
}
func callAsFunction<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) -> NodeLink<Self, Item> {
link(emptyIsNil: emptyIsNil, to: items)
}
func link<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder to items: () -> [Item]) -> NodeLink<Self, Item> {
NodeLink(self, emptyIsNil: emptyIsNil, items: items)
}
func matchedFilterOptions() -> SourceFilter.Options {
return []
}
func matchesTerm(_ term: String) -> Bool {
term.isEmpty || title.uppercased().contains(term.uppercased())
}
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool {
filter.options.isSubset(of: options) && matchesTerm(filter.searchTerm)
}
}
/// Indicates a type that owns a list of Nodes
protocol NodeSource {
associatedtype List: NodeList
var items : List? { get set }
}
/// Defines the requirements of a collection that can serve as a `NodeList`.
protocol NodeList: RandomAccessCollection where Self.Element: Node, Index: Hashable { }
extension NodeList {
@NodeListBuilder
func linkEachTo<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> some NodeList {
map { item in
item.link(emptyIsNil: emptyIsNil, to: { items(item) })
}
// Makes compiler unhappy. resultBuilder probably incorrect
// for item in self {
// item.link(to: { items(item) })
// }
}
}
extension Array: NodeList where Element: Node { }
// MARK: - Special Nodes -
enum LeafNode: Node {
var title : String { "impossible" }
var headerTitle : String { title }
var header: some View { Text("impossible") }
var content: some View { Text("impossible") }
func icon(forHeader: Bool) -> some View {
Text("impossible")
}
}
struct RootNode<Item: Node>: Node {
var items : [Item]?
var title : String { "Root" }
var headerTitle : String { title }
var header : some View { Text("Root") }
var content : some View { Text("Root") }
init() {
self.items = nil
}
init(@NodeListBuilder _ items: () -> [Item]) {
self.items = items()
}
func icon(forHeader: Bool) -> some View { symbolIcon("tree", forHeader: forHeader) }
}
struct NodeLink<Base: Node, Item: Node>: Node {
var base : Base
var items : [Item]?
var title : String { base.title }
var headerTitle : String { base.headerTitle }
var header : Base.Header { base.header }
var content : Base.Content { base.content }
init(_ base: Base, emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) {
let list = items()
self.base = base
self.items = emptyIsNil ? (list.isEmpty ? nil : list) : list
}
@available(*, deprecated, message: "Consider using Root { items } instead")
init(@NodeListBuilder _ items: () -> [Item]) where Base == RootNode<Item> {
self.base = RootNode()
self.items = items()
}
func icon(forHeader: Bool) -> some View {
base.icon(forHeader: forHeader)
}
func matchedFilterOptions() -> SourceFilter.Options {
return base.matchedFilterOptions()
}
}

View file

@ -0,0 +1,173 @@
//
// NodeListBuilder.swift
// NodeItems
//
// Created by Casey Fleser on 3/2/23.
//
import SwiftUI
@resultBuilder struct NodeListBuilder {
typealias P = Node
enum OneOf<A: P, B: P>: P, CustomStringConvertible {
typealias List = [NodeListBuilder.OneOf<A.List.Element, B.List.Element>]
case a(A)
case b(B)
var title : String {
switch self {
case .a(let node): return node.title
case .b(let node): return node.title
}
}
var headerTitle : String {
switch self {
case .a(let node): return node.headerTitle
case .b(let node): return node.headerTitle
}
}
@ViewBuilder
var header : some View {
switch self {
case .a(let node): node.header
case .b(let node): node.header
}
}
@ViewBuilder
var content : some View {
switch self {
case .a(let node): node.content
case .b(let node): node.content
}
}
var items : List? {
get {
switch self {
case .a(let node): return node.items?.map { .a($0) }
case .b(let node): return node.items?.map { .b($0) }
}
}
set { }
}
var description : String {
let valueDesc : String
switch self {
case .a(let node): valueDesc = ".a: \(String(describing: node))"
case .b(let node): valueDesc = ".b: \(String(describing: node))"
}
return "OneOf<A - \(A.self), B - \(B.self)>: \(valueDesc)"
}
func icon(forHeader: Bool) -> some View {
switch self {
case .a(let node): node.icon(forHeader: forHeader)
case .b(let node): node.icon(forHeader: forHeader)
}
}
func matchedFilterOptions() -> SourceFilter.Options {
switch self {
case .a(let node): return node.matchedFilterOptions()
case .b(let node): return node.matchedFilterOptions()
}
}
}
static func buildBlock<C: P>(_ c: [C]) -> [C] {
c
}
static func buildBlock<C0: P, C1: P> (
_ c0: [C0], _ c1: [C1]) -> [OneOf<C0, C1>]
{
[buildEither(first: c0), buildEither(second: c1)].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2]) -> [OneOf<OneOf<C0, C1>, C2>]
{
[buildEither(first: buildBlock(c0, c1)), buildEither(second: c2)].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3]) -> [OneOf<OneOf<C0, C1>, OneOf<C2, C3>>]
{
[buildEither(first: buildBlock(c0, c1)), buildEither(second: buildBlock(c2, c3))].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, C4>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: c4)].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<C4, C5>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5))].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, C6>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6))].flatMap { $0 }
}
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P, C7: P> (
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6], _ c7: [C7]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, OneOf<C6, C7>>>]
{
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6, c7))].flatMap { $0 }
}
// static func buildBlock<C: Node>(_ c: [C]...) -> [C] {
// c.flatMap { $0 }
// }
//
// Same type buildBlocks. This works but buildBlock<C: Node>(_ c: [C]...) -> [C] confuses the compiler
static func buildBlock<C0: Node> (_ c0: [C0], _ c1: [C0]) -> [C0] {
[c0, c1].flatMap { $0 }
}
static func buildBlock<C0: Node> (_ c0: [C0], _ c1: [C0], _ c2: [C0]) -> [C0] {
[c0, c1, c2].flatMap { $0 }
}
static func buildEither<C0: P, C1: P>(first c0: [C0]) -> [OneOf<C0, C1>] {
c0.map { OneOf<C0, C1>.a($0) }
}
static func buildEither<C0: P, C1: P>(second c1: [C1]) -> [OneOf<C0, C1>] {
c1.map { OneOf<C0, C1>.b($0) }
}
static func buildOptional<C: P>(_ c: [C]?) -> [C] {
c ?? []
}
static func buildArray<C: P>(_ c: [[C]]) -> [C] {
c.flatMap { $0 }
}
static func buildExpression<N: Node>(_ node: N) -> [N] {
[node]
}
static func buildExpression<NL: NodeList>(_ nodeList: NL) -> [NL.Element] {
Array(nodeList)
}
static func buildExpression<NS: NodeSource>(_ nodeSource: NS) -> [NS.List.Element] {
nodeSource.items.map({ buildExpression($0) }) ?? []
}
}

View file

@ -0,0 +1,95 @@
//
// FilteredNodeView.swift
// NodeItems
//
// Created by Casey Fleser on 3/3/23.
//
import SwiftUI
struct FilteredNodeView<T: Node>: View {
@StateObject var node : FilteredNode<T>
@Binding var filter : SourceFilter
init(_ node: T, filter: Binding<SourceFilter>) {
self._node = StateObject(wrappedValue: FilteredNode(node))
self._filter = filter
}
init<Item: Node>(filter: Binding<SourceFilter>, @NodeListBuilder items: () -> [Item]) where T == RootNode<Item> {
self.init(RootNode(items), filter: filter)
}
var body: some View {
Root(node: node)
.searchable(text: $filter.searchTerm, placement: .sidebar)
.onAppear { node.applyFilter(filter) }
.onChange(of: filter) { node.applyFilter($0) }
}
}
extension FilteredNodeView {
struct Root: View {
@ObservedObject var node : FilteredNode<T>
var visibleItems : FilteredNode<T>.List { node.items.map { $0.filter { !$0.filtered} } ?? [] }
var body: some View {
let items = visibleItems
List {
if !items.isEmpty {
ForEach(items.indices, id: \.self) { index in
Item(node: items[index])
}
}
else {
Text("No Filter Results")
}
}
}
}
struct ItemList<T: Node>: View {
var items : [FilteredNode<T>]
init(items: [FilteredNode<T>]) {
self.items = items
}
var body: some View {
ForEach(items.indices, id: \.self) { index in
Item(node: items[index])
}
}
}
struct Item<T: Node>: View {
@ObservedObject var node : FilteredNode<T>
var body: some View {
if !node.filtered {
NodeLabel(node)
if let items = node.items, node.isExpanded {
ItemList(items: items)
.padding(.leading, 12.0)
}
}
}
}
}
struct FilteredNodeView_Previews: PreviewProvider {
@State static var filter = SourceFilter.restore()
static var previews: some View {
List {
FilteredNodeView(filter: $filter) {
SimPlatform.iOS
SimPlatform.tvOS
SimPlatform.watchOS
}
}
}
}

View file

@ -0,0 +1,61 @@
//
// NodeLabel.swift
// NodeItems
//
// Created by Casey Fleser on 3/3/23.
//
import SwiftUI
struct NodeLabel<T: Node>: View {
@ObservedObject var node : FilteredNode<T>
init(_ node: FilteredNode<T>) {
self.node = node
}
var body: some View {
HStack(spacing: 0) {
let button =
Button(
action: {
let optionActive = NSApplication.shared.currentEvent?.modifierFlags.contains(.option) == true
withAnimation(.easeInOut(duration: 0.2)) {
node.toggleExpanded(deep: optionActive)
}
},
label: {
Image(systemName: "chevron.right")
.padding(.horizontal, 4.0)
.contentShape(Rectangle())
.rotationEffect(.degrees(node.isExpanded ? 90.0 : 0.0))
}
)
.buttonStyle(.plain)
if node.items != nil { button }
else { button.hidden() }
NavigationLink(
destination: { NodeView(node) },
label: {
Label(
title: { Text(node.title) },
icon: { node.icon(forHeader: false) }
)
}
)
}
}
}
struct NodeLabel_Previews: PreviewProvider {
@StateObject static var previewItem = FilteredNode(SimPlatform.iOS)
static var previews: some View {
VStack {
NodeLabel(previewItem)
}
}
}

View file

@ -1,27 +1,29 @@
//
// SourceItemContent.swift
// NodeView.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
// Created by Casey Fleser on 3/6/23.
//
import SwiftUI
import SwiftUI
struct SourceItemContent<Item: SourceItem>: View {
var item : Item
struct NodeView<Item: Node>: View {
var node : Item
init(_ node: Item) {
self.node = node
}
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0.0) {
// --- Header section ---
VStack(alignment: .leading) {
Text(item.headerTitle)
Text(node.headerTitle)
.font(.system(size: 20))
.padding(.top, 12.0)
.padding(.bottom, 8.0)
item.header
node.header
.padding(.trailing, 136.0)
}
.padding([.leading, .trailing])
@ -31,7 +33,7 @@ struct SourceItemContent<Item: SourceItem>: View {
// --- Content section ---
ScrollView {
HStack {
item.content
node.content
.padding(.top, 4.0)
.padding(.trailing)
Spacer()
@ -41,27 +43,18 @@ struct SourceItemContent<Item: SourceItem>: View {
.padding([.leading, .top])
.background(.background)
}
.overlay(
SourceItemImage(imageDesc: item.imageDesc, isLabelImage: false)
.padding([.top, .trailing], 24.0),
alignment: .topTrailing
)
.overlay(alignment: .topTrailing) {
node.icon(forHeader: true)
.padding([.top, .trailing], 24.0)
}
.padding(.top, -geometry.frame(in: .global).origin.y)
}
.navigationTitle(item.title)
.navigationTitle(node.title)
}
}
struct SourceItemContent_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
struct NodeView_Previews: PreviewProvider {
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemContent(item: item)
.preferredColorScheme(.dark)
SourceItemContent(item: item)
.preferredColorScheme(.light)
}
NodeView(SimPlatform.iOS)
}
}

View file

@ -7,7 +7,7 @@
import Foundation
struct SourceFilter {
struct SourceFilter: Equatable {
struct Options: OptionSet, CaseIterable {
let rawValue: Int
@ -15,27 +15,21 @@ struct SourceFilter {
static let runtimeInstalled = Options(rawValue: 1 << 1)
static var allCases : [Options] = [.withApps, .runtimeInstalled]
func search<T: SourceItem>(item: T, progress: Options) -> Self {
var foundOptions = progress.union(item.data.optionTrait)
if !subtracting(foundOptions).isEmpty {
for child in item.children {
foundOptions = search(item: child, progress: foundOptions)
if isSubset(of: foundOptions) {
break
}
}
}
return foundOptions
}
}
var searchTerm = ""
var options = Options() { didSet { UserDefaults.standard.set(options.rawValue, forKey: "FilterOptions") } }
var filterApps : Bool {
get { options.contains(.withApps) }
set { options.booleanSet(newValue, options: .withApps) }
}
var filterRuntimes : Bool {
get { options.contains(.runtimeInstalled) }
set { options.booleanSet(newValue, options: .runtimeInstalled) }
}
static func restore() -> SourceFilter {
var filter = SourceFilter()

View file

@ -1,95 +0,0 @@
//
// SourceItem.swift
// SimDirs
//
// Created by Casey Fleser on 6/14/22.
//
import SwiftUI
protocol SourceItem: Identifiable, ObservableObject {
associatedtype Model : SourceItemData
associatedtype Child : SourceItem
var id : UUID { get }
var data : Model { get }
var children : [Child] { get set }
var visibleChildren : [Child] { get set }
var customImgDesc : SourceImageDesc? { get }
var isExpanded : Bool { get set }
}
extension SourceItem {
var title : String { data.title }
var headerTitle : String { data.headerTitle }
var header : some View { data.header }
var content : some View { data.content }
var imageDesc : SourceImageDesc { customImgDesc ?? data.imageDesc }
var customImgDesc : SourceImageDesc? { nil }
func applyFilter(_ filter: SourceFilter) {
visibleChildren = children.filter { $0.applyingFilter(filter) }
}
func applyingFilter(_ filter: SourceFilter, inheritedOptions: SourceFilter.Options = []) -> Bool {
var match = true
let optProgress = inheritedOptions.union(data.optionTrait) // options inherited by children
// If there are options to match then do that first passing inherited options along
// and consider a match fulfilled if any child contains all the desired options.
if !filter.options.isEmpty {
var foundOptions = optProgress
if !filter.options.isSubset(of: foundOptions) {
foundOptions = filter.options.search(item: self, progress: foundOptions)
}
match = filter.options.isSubset(of: foundOptions)
}
if !filter.searchTerm.isEmpty && match {
match = title.uppercased().contains(filter.searchTerm.uppercased())
}
visibleChildren = children.filter { $0.applyingFilter(filter, inheritedOptions: optProgress) }
return match || !visibleChildren.isEmpty
}
func toggleExpanded(_ expanded: Bool? = nil, deep: Bool) {
isExpanded = expanded ?? !isExpanded
if deep {
for child in children {
child.toggleExpanded(isExpanded, deep: true)
}
}
}
}
class SourceItemVal<Model: SourceItemData, Child: SourceItem>: SourceItem {
@Published var visibleChildren : [Child]
@Published var isExpanded : Bool
var id = UUID()
var data : Model
var children : [Child]
var customImgDesc : SourceImageDesc?
init(id: UUID = UUID(), data: Model, children: [Child] = [], customImgDesc: SourceImageDesc? = nil) {
self.id = id
self.data = data
self.children = children
self.visibleChildren = children
self.customImgDesc = customImgDesc
self.isExpanded = false
}
}
class SourceItemNone: SourceItem {
var id = UUID()
var data = SourceItemDataNone()
var children = [SourceItemNone]()
var visibleChildren = [SourceItemNone]()
var isExpanded = false
}

View file

@ -1,50 +0,0 @@
//
// SourceItemData.swift
// SimDirs
//
// Created by Casey Fleser on 6/15/22.
//
import SwiftUI
protocol SourceItemData {
associatedtype Content : View
associatedtype Header : View
@ViewBuilder var header : Self.Header { get }
@ViewBuilder var content : Self.Content { get }
var title : String { get }
var headerTitle : String { get }
var imageDesc : SourceImageDesc { get }
var optionTrait : SourceFilter.Options { get }
}
extension SourceItemData {
var headerTitle : String { title }
var imageDesc : SourceImageDesc { .symbol() }
var header : some View { get { EmptyView() } }
var content : some View { get { EmptyView() } }
var optionTrait : SourceFilter.Options { [] }
}
enum SourceImageDesc {
case icon(nsImage: NSImage)
case symbol(systemName: String = "questionmark.circle", color: Color = .primary)
func withColor(_ color: Color) -> Self {
switch self {
case .icon: return self
case let .symbol(name, _): return .symbol(systemName: name, color: color)
}
}
}
struct SourceItemDataNone: SourceItemData {
static let none = SourceItemDataNone()
var title : String { "" }
}

View file

@ -9,29 +9,6 @@ import Foundation
import Combine
class SourceState: ObservableObject {
typealias ProductFamily = SourceItemVal<SimProductFamily, DeviceType_DS>
typealias Platform = SourceItemVal<SimPlatform, Runtime_RT>
typealias DeviceType_DS = SourceItemVal<SimDeviceType, Runtime_DS>
typealias DeviceType_RT = SourceItemVal<SimDeviceType, Device>
typealias Runtime_DS = SourceItemVal<SimRuntime, Device>
typealias Runtime_RT = SourceItemVal<SimRuntime, DeviceType_RT>
typealias Device = SourceItemVal<SimDevice, App>
typealias App = SourceItemVal<SimApp, SourceItemNone>
enum Base: Identifiable {
case placeholder(id: UUID = UUID())
case device(id: UUID = UUID(), SourceItemVal<SourceItemDataNone, ProductFamily>)
case runtime(id: UUID = UUID(), SourceItemVal<SourceItemDataNone, Platform>)
var id : UUID {
switch self {
case let .placeholder(id): return id
case let .device(id, _): return id
case let .runtime(id, _): return id
}
}
}
enum Style: Int, CaseIterable, Identifiable {
case placeholder
case byDevice
@ -53,34 +30,22 @@ class SourceState: ObservableObject {
}
}
@Published var style = Style.placeholder { didSet { rebuildBase() } }
@Published var filter = SourceFilter.restore() { didSet { applyFilter() } }
@Published var style = Style.placeholder // { didSet { rebuildBase() } }
@Published var selection : UUID?
var model : SimModel
var base = Base.placeholder()
var deviceUpdates : Cancellable?
var filterApps : Bool {
get { filter.options.contains(.withApps) }
set { filter.options.booleanSet(newValue, options: .withApps) }
}
var filterRuntimes : Bool {
get { filter.options.contains(.runtimeInstalled) }
set { filter.options.booleanSet(newValue, options: .runtimeInstalled) }
}
init(model: SimModel) {
self.style = .byDevice
self.model = model
self.rebuildBase()
deviceUpdates = model.deviceUpdates.sink(receiveValue: applyDeviceUpdates)
}
#warning("TODO: still need to apply updates")
func applyDeviceUpdates(_ updates: SimDevicesUpdates) {
#if false
switch base {
case .placeholder:
break
@ -122,69 +87,37 @@ class SourceState: ObservableObject {
}
applyFilter()
#endif
}
func applyFilter() {
switch base {
case .placeholder: break
case let .device(_, item): item.applyFilter(filter)
case let .runtime(_, item): item.applyFilter(filter)
}
}
func rebuildBase() {
var baseID : UUID
// Preserve identifier if style is not changing
switch (style, base) {
case (.placeholder, let .placeholder(id)): baseID = id;
case (.byDevice, let .device(id, _)): baseID = id;
case (.byRuntime, let .runtime(id, _)): baseID = id;
default: baseID = UUID()
}
switch style {
case .placeholder: base = .placeholder(id: baseID)
case .byDevice: base = .device(id: baseID, SourceItemVal(data: .none, children: deviceStyleItems()))
case .byRuntime: base = .runtime(id: baseID, SourceItemVal(data: .none, children: runtimeStyleItems()))
}
applyFilter()
}
func baseFor(style: Style) -> Base {
switch style {
case .placeholder: return .placeholder()
case .byDevice: return .device(SourceItemVal(data: .none, children: deviceStyleItems()))
case .byRuntime: return .runtime(SourceItemVal(data: .none, children: runtimeStyleItems()))
}
}
func deviceStyleItems() -> [ProductFamily] {
SimProductFamily.presentation.map { family in
ProductFamily(data: family, children: model.deviceTypes.supporting(productFamily: family).map { devType in
DeviceType_DS(data: devType, children: model.runtimes.supporting(deviceType: devType).map { runtime in
Runtime_DS(data: runtime, children: runtime.devices.of(deviceType: devType).map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
})
})
})
@NodeListBuilder
var items: some NodeList {
switch style {
case .placeholder: [] as [LeafNode]
case .byDevice: deviceStyleItems
case .byRuntime: runtimeStyleItems
}
}
@NodeListBuilder
var deviceStyleItems: some NodeList {
SimProductFamily.presentation.linkEachTo { family in
model.deviceTypes.supporting(productFamily: family).linkEachTo(emptyIsNil: true) { devType in
model.runtimes.supporting(deviceType: devType).linkEachTo(emptyIsNil: true) { runtime in
runtime.devices.linkingDeviceType(devType)
}
}
}
}
func runtimeStyleItems() -> [Platform] {
SimPlatform.presentation.map { platform in
Platform(data: platform, children: model.runtimes.supporting(platform: platform).map { runtime in
Runtime_RT(data: runtime, children: model.deviceTypes.supporting(runtime: runtime).map { devType in
DeviceType_RT(data: devType, children: runtime.devices.of(deviceType: devType).map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
})
})
})
@NodeListBuilder
var runtimeStyleItems: some NodeList {
SimPlatform.presentation.linkEachTo(emptyIsNil: true) { platform in
model.runtimes.supporting(platform: platform).linkEachTo(emptyIsNil: true) { runtime in
model.deviceTypes.supporting(runtime: runtime).linkEachTo(emptyIsNil: true) { devType in
runtime.devices.linkingDeviceType(devType)
}
}
}
}
}

View file

@ -8,8 +8,6 @@
import SwiftUI
extension SimApp {
public var content : some View { AppContent(app: self) }
var isLaunched : Bool {
get { state.isOn }
set { toggleLaunchState() }

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimApp {
public var header : some View { AppHeader(app: self) }
}
struct AppHeader: View {
var app : SimApp

View file

@ -9,8 +9,6 @@ import SwiftUI
import UniformTypeIdentifiers
extension SimDevice {
public var content : some View { DeviceContent(self) }
var scheme : ColorScheme? {
get {
switch appearance {

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimDevice {
public var header : some View { DeviceHeader(device: self) }
}
struct DeviceHeader: View {
@ObservedObject var device : SimDevice

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimDeviceType {
public var content : some View { DeviceTypeContent(deviceType: self) }
}
struct DeviceTypeContent: View {
var deviceType : SimDeviceType

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimDeviceType {
public var header : some View { DeviceTypeHeader(deviceType: self) }
}
struct DeviceTypeHeader: View {
var deviceType : SimDeviceType

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimRuntime {
public var content : some View { RuntimeContent(runtime: self) }
}
struct RuntimeContent: View {
struct SupportedItem: Identifiable {
let name : String

View file

@ -7,10 +7,6 @@
import SwiftUI
extension SimRuntime {
public var header : some View { RuntimeHeader(runtime: self) }
}
struct RuntimeHeader: View {
var runtime : SimRuntime

View file

@ -1,61 +0,0 @@
//
// SourceItemGroup.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemGroup<Item: SourceItem>: View {
@StateObject var item : Item
@Binding var selection : UUID?
var body: some View {
HStack(spacing: 0) {
let button = Button(action: {
let optionActive = NSApplication.shared.currentEvent?.modifierFlags.contains(.option) == true
withAnimation(.easeInOut(duration: 0.2)) {
item.toggleExpanded(deep: optionActive)
}
}, label: {
Image(systemName: "chevron.right")
.padding(.horizontal, 2.0)
.contentShape(Rectangle())
.rotationEffect(.degrees(item.isExpanded ? 90.0 : 0.0))
})
.buttonStyle(.plain)
if item.visibleChildren.count == 0 {
button.hidden()
}
else {
button
}
SourceItemLink(selection: $selection, item: item)
}
if item.isExpanded {
ForEach(item.visibleChildren) { childItem in
SourceItemGroup<Item.Child>(item: childItem, selection: $selection)
}
.padding(.leading, 12.0)
}
}
}
struct SourceItemGroup_Previews: PreviewProvider {
@State static var selection : UUID?
static var state = SourceState(model: SimModel())
static var sampleItem = state.deviceStyleItems()[0]
static var previews: some View {
List {
SourceItemGroup(item: sampleItem, selection: $selection)
SourceItemGroup(item: sampleItem, selection: $selection)
SourceItemGroup(item: sampleItem, selection: $selection)
}
}
}

View file

@ -1,54 +0,0 @@
//
// SourceItemImage.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemImage: View {
var imageDesc : SourceImageDesc
var isLabelImage = true
var imageSize : CGFloat?
var body: some View {
let size = imageSize ?? (isLabelImage ? 20.0 : 128.0)
switch imageDesc {
case let .icon(nsImage):
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: size, maxHeight: size)
.cornerRadius(size / 5.0)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
case let .symbol(systemName, color):
if isLabelImage {
Image(systemName: systemName)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
}
else {
Image(systemName: systemName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: size, maxHeight: size)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
}
}
}
}
struct SourceItemImage_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemImage(imageDesc: item.imageDesc)
SourceItemImage(imageDesc: item.imageDesc, isLabelImage: false)
}
}
}

View file

@ -1,31 +0,0 @@
//
// SourceItemLabel.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemLabel<Item: SourceItem>: View {
var item : Item
var body: some View {
Label(
title: { Text(item.title) },
icon: { SourceItemImage(imageDesc: item.imageDesc) }
)
}
}
struct SourceItemLabel_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemLabel(item: item)
SourceItemLabel(item: item)
}
}
}

View file

@ -1,34 +0,0 @@
//
// SourceItemLink.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemLink<Item: SourceItem>: View {
@Binding var selection: UUID?
var item : Item
var body: some View {
NavigationLink(tag: item.id, selection: $selection,
destination: { SourceItemContent(item: item) },
label: { SourceItemLabel(item: item) }
)
}
}
struct SourceItemLink_Previews: PreviewProvider {
@State static var selection : UUID?
static var state = SourceState(model: SimModel())
static var sampleItem = state.deviceStyleItems()[0]
static var previews: some View {
SourceItemLink(selection: $selection, item: sampleItem)
.preferredColorScheme(.dark)
SourceItemLink(selection: $selection, item: sampleItem)
.preferredColorScheme(.light)
}
}

View file

@ -9,6 +9,7 @@ import SwiftUI
struct ToolbarMenu: View {
@ObservedObject var state : SourceState
@Binding var filter : SourceFilter
var body: some View {
Menu {
@ -20,8 +21,8 @@ struct ToolbarMenu: View {
}
}
.pickerStyle(.inline)
Toggle(isOn: $state.filterApps) { Label("With Apps", systemImage: "app.fill") }
Toggle(isOn: $state.filterRuntimes) { Label("Installed Runtimes", systemImage: "cpu.fill") }
Toggle(isOn: $filter.filterApps) { Label("With Apps", systemImage: "app.fill") }
Toggle(isOn: $filter.filterRuntimes) { Label("Installed Runtimes", systemImage: "cpu.fill") }
} label: {
Label("Filter", systemImage: "slider.horizontal.3")
}
@ -29,10 +30,10 @@ struct ToolbarMenu: View {
}
struct ToolbarMenu_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var state = SourceState(model: SimModel())
@State static var filter = SourceFilter.restore()
static var previews: some View {
ToolbarMenu(state: state)
ToolbarMenu(state: state, filter: $filter)
}
}