diff --git a/Sources/Core/Menu/MenuCollection.swift b/Sources/Core/Menu/MenuCollection.swift index 296526b..5e4a647 100644 --- a/Sources/Core/Menu/MenuCollection.swift +++ b/Sources/Core/Menu/MenuCollection.swift @@ -56,7 +56,7 @@ public struct MenuCollection: MenuWidget, Wrapper { /// - data: The widget data. /// - menu: The menu. /// - Returns: The view storage with the GMenu as the pointer. - public func getMenu(data: WidgetData, menu: NSMenu?) -> ViewStorage { + public func getMenu(data: WidgetData, menu: NSMenu? = nil) -> ViewStorage { let item = NSMenuItem() let menu = menu ?? .init() let storage = container(data: data.noModifiers, type: MenuContext.self) diff --git a/Sources/Core/View/MenuWrapper.swift b/Sources/Core/View/MenuWrapper.swift new file mode 100644 index 0000000..ea98ede --- /dev/null +++ b/Sources/Core/View/MenuWrapper.swift @@ -0,0 +1,77 @@ +// +// MenuWrapper.swift +// MacBackend +// +// Created by david-swift on 06.12.2024. +// + +import AppKit + +/// Wrap a view to add a menu. +public struct MenuWrapper: MacWidget { + + /// Whether the menu is visible. + @Binding var isPresented: Bool + /// The content. + var content: Body + /// The menu. + var menu: Body + + /// Initialize a menu wrapper. + /// - Parameters: + /// - content: The view content. + /// - isPresented: Whether the menu is visible. + /// - menu: The menu. + public init(@ViewBuilder content: () -> Body, isPresented: Binding, @ViewBuilder menu: () -> Body) { + self._isPresented = isPresented + self.content = content() + self.menu = menu() + } + + /// The view storage. + /// - Parameters: + /// - data: The widget data. + /// - type: The type of the views. + /// - Returns: The view storage. + public func container( + data: WidgetData, + type: Data.Type + ) -> ViewStorage where Data: ViewRenderData { + let content = content.storage(data: data, type: type) + let menuPointer = NSMenu() + let menu = MenuCollection { self.menu }.getMenu(data: data, menu: menuPointer) + let storage = ViewStorage(content.pointer, content: [.mainContent: [content], "menu": [menu]]) + storage.fields["menu"] = menuPointer + update(storage, data: data, updateProperties: true, type: type) + return storage + } + + /// Update the stored content. + /// - Parameters: + /// - storage: The storage to update. + /// - data: The widget data. + /// - updateProperties: Whether to update the properties. + /// - type: The type of the views. + public func update( + _ storage: ViewStorage, + data: WidgetData, + updateProperties: Bool, + type: Data.Type + ) where Data: ViewRenderData { + if let content = storage.content[.mainContent]?.first { + self.content.updateStorage(content, data: data, updateProperties: updateProperties, type: type) + } + if let menu = storage.content["menu"]?.first { + MenuCollection { self.menu } + .updateStorage(menu, data: data, updateProperties: updateProperties, type: MenuContext.self) + } + if isPresented, + let menu = storage.fields["menu"] as? NSMenu, + let content = storage.content[.mainContent]?.first?.pointer as? NSView, + let currentEvent = NSApp.currentEvent { + NSMenu.popUpContextMenu(menu, with: currentEvent, for: content) + isPresented = false + } + } + +} diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift index 6be2a36..63b94bb 100644 --- a/Sources/Demo/Demo.swift +++ b/Sources/Demo/Demo.swift @@ -24,6 +24,7 @@ struct Demo: App { @State private var elements: [Element] = [.init()] @State private var selectedElement: String? @State private var alert = false + @State private var menuPresented = false var scene: Scene { Window("Main", id: "main") { @@ -39,7 +40,12 @@ struct Demo: App { selectedElement = element.id } Button(alert.description) { - alert = true + menuPresented = true + } + .menu(isPresented: $menuPresented) { + MenuButton(alert.description) { + print("Hi") + } } } .alert("Hello", isPresented: $alert) diff --git a/Sources/MacBackend/AnyView.swift b/Sources/MacBackend/AnyView.swift index b19666a..4e9fe3f 100644 --- a/Sources/MacBackend/AnyView.swift +++ b/Sources/MacBackend/AnyView.swift @@ -30,4 +30,13 @@ extension AnyView { PaddingView(padding: padding, edges: edges, child: self) } + /// A menu over the view. + /// - Parameters: + /// - isPresented: Whether the menu is currently visible. + /// - menu: The menu. + /// - Returns: The view. + public func menu(isPresented: Binding, @ViewBuilder menu: @escaping () -> Body) -> Meta.AnyView { + MenuWrapper(content: { self }, isPresented: isPresented, menu: menu) + } + }