diff --git a/.github/workflows/ci-macos-appkit.yml b/.github/workflows/ci-macos-appkit.yml new file mode 100644 index 000000000..bd209dc22 --- /dev/null +++ b/.github/workflows/ci-macos-appkit.yml @@ -0,0 +1,36 @@ +name: CI - macOS AppKit + +on: + push: + branches: [main] + paths: + - 'platforms/MacOS/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main] + paths: + - 'platforms/MacOS/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + +jobs: + build: + uses: ./.github/workflows/_build.yml + with: + project-path: platforms/MacOS/MacOS.slnx + project-name: macos-appkit + os: '["macos-latest"]' + install-workloads: true + run-tests: false + pack: true + xcode-version: '26.3' diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f9abb3ba..7da8ccd4f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,9 @@ + + @@ -40,6 +42,7 @@ + @@ -51,9 +54,6 @@ - - - diff --git a/MauiLabs.slnx b/MauiLabs.slnx index dcbaa0c90..a3e2c9c07 100644 --- a/MauiLabs.slnx +++ b/MauiLabs.slnx @@ -55,6 +55,11 @@ + + + + + diff --git a/README.md b/README.md index a23a01ec5..aeb76f64e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,22 @@ A comprehensive MAUI testing, automation, and debugging toolkit. The DevFlow CLI | `Microsoft.Maui.DevFlow.Driver` | Platform driver library | | `Microsoft.Maui.DevFlow.Logging` | Buffered JSONL file logger | +### macOS AppKit Backend + +A native macOS AppKit backend for .NET MAUI — run MAUI apps as true AppKit apps with NSWindow, NSButton, NSScrollView, native menu bar, sidebar flyout, and more. An alternative to Mac Catalyst. + +- **Native AppKit controls** — NSTextField, NSButton, NSSwitch, NSSlider, NSImageView, and more +- **Navigation** — Shell, NavigationPage, TabbedPage, FlyoutPage with sidebar +- **Blazor WebView** — via WKWebView +- **MapKit** — native MapView integration +- **Essentials** — AppInfo, Battery, Clipboard, Geolocation, Preferences, SecureStorage, Sensors + +| Package | Description | +|---------|-------------| +| `Microsoft.Maui.Platforms.MacOS` | Core AppKit backend — handlers, hosting, MapKit | +| `Microsoft.Maui.Platforms.MacOS.Essentials` | Essentials APIs for macOS | +| `Microsoft.Maui.Platforms.MacOS.BlazorWebView` | Blazor Hybrid via WKWebView | + ### WPF Backend A WPF-based alternative to the official WinUI backend for .NET MAUI. Run MAUI apps on Windows desktops using native WPF controls with 22+ fully implemented controls, Shell navigation, Blazor WebView, and 14 Essentials APIs. diff --git a/eng/Versions.props b/eng/Versions.props index 4b07aa57b..5c9d1e8f1 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -19,6 +19,7 @@ 10.0.20 $(MicrosoftMauiControlsVersion) $(MicrosoftMauiControlsVersion) + 10.0.3 $(MicrosoftMauiControlsVersion) $(MicrosoftMauiControlsVersion) @@ -34,6 +35,7 @@ + 5.0.0 1.3.1 4.12.0 3.119.2 @@ -56,14 +58,14 @@ 1.0.0-preview.1.26230.1 - 0.3.0 - $(PlatformMauiMacOSVersion) - $(PlatformMauiMacOSVersion) 0.6.0 $(PlatformMauiLinuxGtk4Version) $(PlatformMauiLinuxGtk4Version) 0.1.0 $(PlatformMauiWindowsWpfVersion) + 0.3.0 + $(PlatformMauiMacOSVersion) + $(PlatformMauiMacOSVersion) diff --git a/eng/pipelines/devflow-official.yml b/eng/pipelines/devflow-official.yml index 4b5be6eb9..76e6f0115 100644 --- a/eng/pipelines/devflow-official.yml +++ b/eng/pipelines/devflow-official.yml @@ -30,6 +30,12 @@ parameters: default: false - name: publishWpfNuget displayName: 'Publish WPF packages to NuGet.org' + type: boolean + default: false +- name: publishMacOSNuget + displayName: 'Publish macOS AppKit packages to NuGet.org' + type: boolean + default: false - name: publishEssentialsAINuget displayName: 'Publish EssentialsAI packages to NuGet.org' type: boolean @@ -326,6 +332,35 @@ extends: -projects $(Build.SourcesDirectory)\platforms\Windows.WPF\Windows.WPF.slnx $(_OfficialBuildArgs) displayName: Build, Test and Pack WPF + + # macOS AppKit packages target net10.0-macos — must build on macOS. + # Signing runs in the Arcade post-build stage (which validates on Windows), + # so the macOS-built nupkgs flow through the standard artifact pipeline. + - job: MacOS + displayName: macOS AppKit - macOS + pool: + name: Azure Pipelines + vmImage: macos-latest + strategy: + matrix: + Release: + _BuildConfig: Release + _OfficialBuildArgs: /p:DotNetSignType=$(_SignType) + /p:TeamName=$(_TeamName) + /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + steps: + - task: UseDotNet@2 + displayName: Install .NET SDK + inputs: + useGlobalJson: true + - script: dotnet workload install maui macos + displayName: Install MAUI and macOS workloads + - script: ./eng/common/cibuild.sh + --configuration $(_BuildConfig) + --prepareMachine + --projects $(Build.SourcesDirectory)/platforms/MacOS/MacOS.slnx + $(_OfficialBuildArgs) + displayName: Build and Pack macOS AppKit - template: /eng/common/templates-official/post-build/post-build.yml@self parameters: @@ -631,3 +666,58 @@ extends: packageParentPath: '$(Pipeline.Workspace)/EssentialsAIPackages' nuGetFeedType: external publishFeedCredentials: 'nuget.org (dotnetframework)' + + # Publish macOS AppKit packages to NuGet.org + - ${{ if eq(parameters.publishMacOSNuget, true) }}: + - stage: publish_macos_nuget + displayName: 'Publish macOS AppKit to NuGet.org' + dependsOn: + - Validate + - publish_using_darc + jobs: + - job: PrepareArtifacts + displayName: 'Prepare macOS Artifacts' + timeoutInMinutes: 15 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + outputs: + - output: pipelineArtifact + displayName: Publish macOS Packages + targetPath: '$(Pipeline.Workspace)/MacOSPackages' + artifactName: MacOSPackagesForNuGet + steps: + - download: current + artifact: PackageArtifacts + displayName: Download PackageArtifacts + - powershell: | + New-Item -ItemType Directory -Force -Path '$(Pipeline.Workspace)/MacOSPackages' + Copy-Item '$(Pipeline.Workspace)/PackageArtifacts/Microsoft.Maui.Platforms.MacOS.*.nupkg' '$(Pipeline.Workspace)/MacOSPackages/' -Verbose -ErrorAction Stop + displayName: Filter macOS packages + + - job: PublishNuGet + displayName: 'Push macOS to NuGet.org' + dependsOn: PrepareArtifacts + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: MacOSPackagesForNuGet + targetPath: '$(Pipeline.Workspace)/MacOSPackages' + steps: + - task: 1ES.PublishNuget@1 + displayName: 'Push macOS to NuGet.org' + inputs: + useDotNetTask: false + packagesToPush: '$(Pipeline.Workspace)/MacOSPackages/*.nupkg' + packageParentPath: '$(Pipeline.Workspace)/MacOSPackages' + nuGetFeedType: external + publishFeedCredentials: 'nuget.org (dotnetframework)' diff --git a/platforms/MacOS/Directory.Build.props b/platforms/MacOS/Directory.Build.props new file mode 100644 index 000000000..83aa668ab --- /dev/null +++ b/platforms/MacOS/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + preview + enable + enable + $(MicrosoftMauiControlsVersion) + + $(WarningsNotAsErrors);CS0067;CS0108;CS0414;CS0612;CS0618;CS8601;CS8602;CS8603;CS8604;CS8619;CS8625;CS8765;CA1416;CA1422;IL2070 + + false + + + + $(DefineConstants);MAUIDEVFLOW + + + + + Microsoft + MIT + https://github.com/dotnet/maui-labs/tree/main/platforms/MacOS + https://github.com/dotnet/maui-labs.git + git + README.md + + false + + diff --git a/platforms/MacOS/Directory.Build.targets b/platforms/MacOS/Directory.Build.targets new file mode 100644 index 000000000..0b1760d19 --- /dev/null +++ b/platforms/MacOS/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/platforms/MacOS/MacOS.slnx b/platforms/MacOS/MacOS.slnx new file mode 100644 index 000000000..efeab01fe --- /dev/null +++ b/platforms/MacOS/MacOS.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/platforms/MacOS/README.md b/platforms/MacOS/README.md new file mode 100644 index 000000000..5833a72e4 --- /dev/null +++ b/platforms/MacOS/README.md @@ -0,0 +1,141 @@ +# .NET MAUI for macOS (AppKit) + +A native [.NET MAUI](https://dot.net/maui) backend for macOS using AppKit — not Mac Catalyst. + +This backend lets MAUI applications run as true native macOS apps that use AppKit controls +(`NSWindow`, `NSButton`, `NSScrollView`, etc.) and follow standard macOS UI conventions +(menu bar, toolbar, sidebar flyout, native dialogs, etc.). + +> **Inspiration:** Originally based on the +> [shinyorg/mauiplatforms](https://github.com/shinyorg/mauiplatforms) project. The Xamarin.Forms +> [`Xamarin.Forms.Platform.MacOS`](https://github.com/xamarin/Xamarin.Forms/tree/5.0.0/Xamarin.Forms.ControlGallery.MacOS) +> backend is also a useful historical reference for AppKit control mappings, although this +> project uses MAUI's modern handler architecture rather than the legacy renderer model. + +## Packages + +| Package | Description | +| --- | --- | +| `Microsoft.Maui.Platforms.MacOS` | Core handlers, hosting, platform services | +| `Microsoft.Maui.Platforms.MacOS.Essentials` | MAUI Essentials implementations (clipboard, preferences, sensors, …) | +| `Microsoft.Maui.Platforms.MacOS.BlazorWebView` | Blazor Hybrid (`BlazorWebView`) support | + +## Prerequisites + +- .NET 10 SDK +- macOS 14 (Sonoma) or later +- Xcode command line tools (for `sips` / `iconutil` — used by the icon build target) + +## Quick start + +### 1. Project file + +```xml + + + net10.0-macos + Exe + true + true + 14.0 + + My macOS App + com.example.myapp + + + + + + + + + + + + +``` + +### 2. `Main.cs` + +```csharp +using AppKit; + +public class MainClass +{ + static void Main(string[] args) + { + NSApplication.Init(); + NSApplication.SharedApplication.Delegate = new MauiMacOSApp(); + NSApplication.Main(args); + } +} +``` + +### 3. `MauiMacOSApp.cs` + +```csharp +using Foundation; +using Microsoft.Maui.Platforms.MacOS; + +[Register("MauiMacOSApp")] +public class MauiMacOSApp : MacOSMauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} +``` + +### 4. `MauiProgram.cs` + +```csharp +using Microsoft.Maui.Platforms.MacOS.Hosting; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiAppMacOS() + .AddMacOSEssentials() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + return builder.Build(); + } +} +``` + +### 5. `App.cs` + +```csharp +public class App : Application +{ + protected override Window CreateWindow(IActivationState? activationState) + => new Window(new MainPage()); +} +``` + +## Building / running the sample + +```bash +dotnet build platforms/MacOS/MacOS.slnx +dotnet run --project platforms/MacOS/samples/MacOS.Sample/ +``` + +## MAUI DevFlow integration + +The sample app supports the optional in-process MAUI DevFlow agent: + +```bash +dotnet run --project platforms/MacOS/samples/MacOS.Sample/ -p:EnableMauiDevFlow=true +``` + +This exposes a local HTTP API and MCP server for inspecting the running app's visual tree, +capturing screenshots, automating interactions, and more. See `src/DevFlow/` and the +`maui-platform-backend` skill's `devflow-integration.md` reference for details. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/platforms/MacOS/docs/README.md b/platforms/MacOS/docs/README.md new file mode 100644 index 000000000..788ab1077 --- /dev/null +++ b/platforms/MacOS/docs/README.md @@ -0,0 +1,40 @@ +# macOS AppKit MAUI Backend — docs + +This directory contains design and reference documentation for the +`Microsoft.Maui.Platforms.MacOS` backend (and its Essentials / BlazorWebView companions). + +## Layout + +- [`handler-audit-status.md`](handler-audit-status.md) — implementation status / audit + carried over from the upstream `shinyorg/mauiplatforms` checklist. Tracks which MAUI + controls have native AppKit handlers and what's still TODO. +- [`guides/`](guides/) — feature guides (MenuBar, Toolbar, Sidebar, Theming, Lifecycle, + Window, Blazor Hybrid, etc.). + +## Quick start + +See the platform [README](../README.md) for a minimal `MauiProgram.CreateMauiApp()` +example using `UseMauiAppMacOS()`. + +## Origin + +The implementation was migrated from +[shinyorg/mauiplatforms](https://github.com/shinyorg/mauiplatforms) (commit `62d4022`) +and refactored to match the conventions of the canonical `Linux.Gtk4` backend in this +repository: + +| Upstream (shinyorg) | maui-labs | +| --- | --- | +| `Platform.Maui.MacOS` | `Microsoft.Maui.Platforms.MacOS` | +| `Platform.Maui.MacOS.Essentials` | `Microsoft.Maui.Platforms.MacOS.Essentials` | +| `Platform.Maui.MacOS.BlazorWebView` | `Microsoft.Maui.Platforms.MacOS.BlazorWebView` | +| `Microsoft.Maui.Platform.MacOS[.Sub]` namespace | `Microsoft.Maui.Platforms.MacOS[.Sub]` namespace | +| `MacOS*` class prefix | `MacOS*` (preserved) | +| `UseMauiAppMacOS()` | `UseMauiAppMacOS()` (unchanged) | + +Folder-level changes (to match the canonical Gtk4 layout): + +- `Dispatching/MacOSDispatcher*.cs` collapsed into `Platform/`. +- `Hosting/MacOSMauiApplication.cs`, `Hosting/MacOSMauiContext*.cs`, and + `Handlers/MacOSFontNamedSizeService.cs` moved into `Platform/`. +- `build/*.targets` ship from `buildTransitive/`. diff --git a/platforms/MacOS/docs/guides/blazor-hybrid.md b/platforms/MacOS/docs/guides/blazor-hybrid.md new file mode 100644 index 000000000..ef6e13a86 --- /dev/null +++ b/platforms/MacOS/docs/guides/blazor-hybrid.md @@ -0,0 +1,157 @@ +# Blazor Hybrid + +Host Blazor components in a native macOS WKWebView using the BlazorWebView control. + +## Setup + +### 1. Add the NuGet Package + +```xml + + +``` + +### 2. Register the Handler + +```csharp +// MauiProgram.cs +using Microsoft.Maui.Platforms.MacOS.Hosting; + +public static MauiApp CreateMauiApp() +{ + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiAppMacOS() + .AddMacOSBlazorWebView(); // Register BlazorWebView handler + + return builder.Build(); +} +``` + +### 3. Link wwwroot Resources + +In your macOS app head `.csproj`, link the Blazor static assets: + +```xml + + + +``` + +The `wwwroot/` folder should contain your `index.html` and any static assets (CSS, JS, images). + +### 4. Use BlazorWebView in a Page + +```csharp +using Microsoft.Maui.Platforms.MacOS.Controls; + +var blazorView = new MacOSBlazorWebView +{ + HostPage = "wwwroot/index.html", +}; + +blazorView.RootComponents.Add(new RootComponent +{ + Selector = "#app", + ComponentType = typeof(MyBlazorApp.Main), +}); + +Content = blazorView; +``` + +## How It Works + +- **WKWebView**: Blazor components are rendered inside a native `WKWebView` +- **Asset loading**: The `MacOSMauiAssetFileProvider` loads files from the app bundle +- **JavaScript interop**: Full Blazor JS interop support via the WebView bridge +- **Threading**: `MacOSBlazorDispatcher` ensures UI updates run on the main AppKit thread + +## Conditional Compilation + +If your shared project has Blazor pages that should only be available on macOS: + +```xml + + + $(DefineConstants);MACAPP + +``` + +```csharp +#if MACAPP +// Register Blazor-specific Shell routes +shell.Items.Add(new ShellContent +{ + Title = "Blazor", + Route = "blazor", + ContentTemplate = new DataTemplate(typeof(BlazorPage)), +}); +#endif +``` + +## Debugging with MauiDevFlow + +If you have [MauiDevFlow](https://github.com/Redth/MauiDevFlow) set up, you can inspect Blazor WebView content via CDP: + +```bash +maui-devflow cdp snapshot # View DOM as accessible text +maui-devflow cdp Runtime evaluate "document.title" # Run JS +``` + +## Content Insets + +The `ContentInsets` property controls how the WebView's scrollable content is positioned within its bounds. Content scrolls through the full `WKWebView` area, but the scroll indicators and initial content position are inset by the specified amounts. + +This is useful when your BlazorWebView extends behind the toolbar or titlebar (e.g., with `FullSizeContentView` enabled) — you can inset the top so content isn't obscured. + +### Usage + +```csharp +var blazorView = new MacOSBlazorWebView +{ + HostPage = "wwwroot/index.html", + ContentInsets = new Thickness(0, 52, 0, 0), // 52pt top inset (toolbar height) +}; +``` + +### Dynamic Updates + +Content insets can be changed at runtime and the WebView will update immediately: + +```csharp +blazorView.ContentInsets = new Thickness(0, 38, 0, 0); // Adjust for compact toolbar +``` + +### How It Works + +- Uses `_topContentInset` on WKWebView (macOS 14+) for top insets +- Uses `ObscuredContentInsets` via `NSEdgeInsets` when available (future macOS versions) for all edges +- The `Thickness` maps to `NSEdgeInsets(top, left, bottom, right)` +- Scroll indicators honor the insets — they won't appear behind obscuring UI + +> **Note:** On current macOS versions, only the **top** inset is reliably supported. Left, bottom, and right insets will take effect when `ObscuredContentInsets` becomes available in a future macOS SDK. + +## Titlebar Drag (FullSizeContentView) + +When `FullSizeContentView` is enabled (the default), the `WKWebView` extends behind the toolbar and can intercept mouse events in the titlebar area, making the window undraggable from the content region. + +The `BlazorWebViewHandler` automatically installs a transparent drag overlay that: + +- Captures mouse events in the titlebar zone and initiates `Window.PerformWindowDrag()` +- Supports double-click to zoom the window +- Passes all other mouse events through to the WKWebView below + +This happens automatically — no configuration needed. If you disable `FullSizeContentView`, the overlay is not installed. + +## Transparent Background + +To let the native window background show through the WebView, set the HTML body background to `transparent`: + +```css +body { + background-color: transparent; +} +``` + +The handler automatically sets `drawsBackground = false` on the underlying `WKWebView`, so transparent CSS backgrounds work out of the box. diff --git a/platforms/MacOS/docs/guides/controls.md b/platforms/MacOS/docs/guides/controls.md new file mode 100644 index 000000000..1248b20b0 --- /dev/null +++ b/platforms/MacOS/docs/guides/controls.md @@ -0,0 +1,162 @@ +# Controls & Platform Notes + +Platform-specific control behaviors, custom controls, and macOS-specific features. + +## App Icons + +The `MauiIcon` build item is automatically converted to a macOS `.icns` file at build time. No manual icon generation is needed. + +```xml + +``` + +The build targets use `sips` and `iconutil` to generate all required sizes: + +| Size | File | +|------|------| +| 16×16 | `icon_16x16.png` | +| 32×32 | `icon_16x16@2x.png`, `icon_32x32.png` | +| 64×64 | `icon_32x32@2x.png` | +| 128×128 | `icon_128x128.png` | +| 256×256 | `icon_128x128@2x.png`, `icon_256x256.png` | +| 512×512 | `icon_256x256@2x.png`, `icon_512x512.png` | +| 1024×1024 | `icon_512x512@2x.png` | + +The `CFBundleIconFile` entry is automatically injected into `Info.plist`. + +## Modal Pages + +Modal pages are presented as sheet-style overlays with: + +- Semi-transparent backdrop (40% black) +- `NSVisualEffectView` with `WindowBackground` material for vibrancy +- 20px inset from safe area edges +- 10pt rounded corners +- Automatic light/dark mode adaptation + +```csharp +await Navigation.PushModalAsync(new MyModalPage()); +await Navigation.PopModalAsync(); +``` + +Multiple modals can be stacked — each new modal overlays the previous one. + +## MapView + +A custom `MapView` control wrapping `MKMapView`: + +```csharp +using Microsoft.Maui.Platforms.MacOS.Controls; + +var map = new MapView +{ + Latitude = 47.6062, + Longitude = -122.3321, + LatitudeDelta = 0.05, + LongitudeDelta = 0.05, + MapType = MapType.Standard, + IsScrollEnabled = true, + IsZoomEnabled = true, + IsShowingUser = true, +}; + +// Add overlays +map.Pins.Add(new MapPin { Latitude = 47.6062, Longitude = -122.3321, Title = "Seattle" }); +map.Circles.Add(new MapCircle { Center = new Location(47.6, -122.3), Radius = 500 }); +map.Polylines.Add(new MapPolyline { Points = { ... } }); +map.Polygons.Add(new MapPolygon { Points = { ... } }); +``` + +### MapType Values + +| Value | Description | +|-------|-------------| +| `Standard` | Street map | +| `Satellite` | Satellite imagery | +| `Hybrid` | Streets overlaid on satellite | + +## Gesture Recognizers + +All MAUI gesture recognizers are supported via `NSGestureRecognizer` wrappers: + +| MAUI Gesture | macOS Implementation | +|-------------|---------------------| +| `TapGestureRecognizer` | `NSClickGestureRecognizer` with configurable tap count | +| `PanGestureRecognizer` | `NSPanGestureRecognizer` with velocity tracking | +| `SwipeGestureRecognizer` | Custom four-direction swipe detection | +| `PinchGestureRecognizer` | `NSMagnificationGestureRecognizer` | +| `PointerGestureRecognizer` | `NSTrackingArea` for enter/exit/move events | + +## Coordinate System + +macOS AppKit uses a bottom-left origin coordinate system, but the platform automatically handles the conversion: + +- All `NSView` subclasses used by the platform override `IsFlipped = true` to use MAUI's top-left origin +- Layout and hit-testing work with MAUI coordinates — no manual conversion needed + +## Multi-Window Support + +The platform supports `Application.OpenWindow()` for creating multiple windows: + +```csharp +Application.Current.OpenWindow(new Window(new SecondPage())); +``` + +Each window gets its own toolbar state, title, and lifecycle events. + +## Fonts + +- Default system font: SF Pro Display (13pt) +- Custom fonts registered via `MauiFont` are loaded with `CTFontManager` +- Font weight mapping supports Bold and Light traits + +```csharp +builder.ConfigureFonts(fonts => +{ + fonts.AddFont("MyFont-Regular.ttf", "MyFont"); + fonts.AddFont("MyFont-Bold.ttf", "MyFontBold"); +}); +``` + +## Text Input + +- `Entry` maps to `NSTextField` +- `Editor` maps to `NSTextView` in an `NSScrollView` +- `SearchBar` maps to `NSSearchField` + +All support standard macOS text editing behaviors (spell check, auto-correct, Services menu). + +## Dispatcher & Threading + +The platform provides a main-thread dispatcher using `DispatchQueue.MainQueue`. + +> **⚠️ Important:** `MainThread.BeginInvokeOnMainThread()` and `MainThread.InvokeOnMainThreadAsync()` from `Microsoft.Maui.ApplicationModel` will throw `NotImplementedInReferenceAssemblyException` on macOS AppKit. Use `Dispatcher` or `IDispatcher` instead: + +```csharp +// ✅ Correct — use Dispatcher (available on any BindableObject / View) +Dispatcher.Dispatch(() => +{ + myLabel.Text = "Updated"; +}); + +// ✅ Correct — use IDispatcher from DI +var dispatcher = serviceProvider.GetRequiredService(); +dispatcher.Dispatch(() => { /* UI work */ }); + +// ✅ Correct — use MainThreadHelper from Essentials package +MainThreadHelper.BeginInvokeOnMainThread(() => { /* UI work */ }); + +// ❌ Throws NotImplementedInReferenceAssemblyException +// MainThread.BeginInvokeOnMainThread(() => { }); +``` + +The `MainThreadHelper` class in `Microsoft.Maui.Platforms.MacOS.Essentials` provides a familiar static API as an alternative: + +```csharp +using Microsoft.Maui.Platforms.MacOS.Essentials; + +if (!MainThreadHelper.IsMainThread) + MainThreadHelper.BeginInvokeOnMainThread(UpdateUI); +``` + +Animation tickers use `NSTimer` for smooth AppKit-thread animation timing. diff --git a/platforms/MacOS/docs/guides/getting-started.md b/platforms/MacOS/docs/guides/getting-started.md new file mode 100644 index 000000000..43b3ac0fd --- /dev/null +++ b/platforms/MacOS/docs/guides/getting-started.md @@ -0,0 +1,281 @@ +# Getting Started + +Set up a .NET MAUI macOS (AppKit) app from scratch, or add macOS support to an existing MAUI project. + +## Overview + +The macOS backend runs as a separate **app head project** that references your shared MAUI code. This is similar to how MAUI uses platform-specific head projects, but since the macOS/AppKit backend is a community package (not built into the MAUI workload), you create a standalone `net10.0-macos` project that links to your shared pages, resources, and platform code. + +``` +MyApp/ +├── MyApp/ # Shared MAUI project (pages, view models, etc.) +│ ├── Pages/ +│ ├── Resources/ +│ │ ├── Fonts/ +│ │ └── AppIcon/ +│ ├── wwwroot/ # (if using Blazor Hybrid) +│ └── Platforms/ +│ └── macOS/ # macOS platform-specific code +│ ├── App.cs +│ ├── Main.cs +│ ├── MauiMacOSApp.cs +│ └── MauiProgram.cs +│ +├── MyApp.MacOS/ # macOS app head project +│ ├── MyApp.MacOS.csproj +│ ├── Info.plist # (optional — auto-generated if absent) +│ └── Entitlements.plist # (optional — for sandboxing, etc.) +│ +└── MyApp.sln +``` + +## Step 1: Create the macOS App Head Project + +Create a new class library targeting `net10.0-macos`: + +```xml + + + + + Exe + net10.0-macos + 14.0 + MyApp + + + false + false + + + + + + + + + +``` + +> **Note:** If referencing the platform project source instead of NuGet, use `` instead of ``. + +## Step 2: Link Shared Code + +Link your shared pages, view models, and helpers from the main MAUI project: + +```xml + + + + + + + + + + + + + + +``` + +### Link Resources + +```xml + + + + + + + + + + +``` + +### App Icon + +The `MauiIcon` item is automatically processed into a macOS `.icns` file at build time. The build targets use `sips` and `iconutil` to generate all required icon sizes (16×16 through 512×512 with @2x variants) from your source PNG or SVG. + +## Step 3: Create Platform Bootstrap Files + +Create these 4 files in your shared project's `Platforms/macOS/` folder. + +### Main.cs — Entry Point + +```csharp +// Platforms/macOS/Main.cs +using AppKit; + +namespace MyApp; + +static class MainClass +{ + static void Main(string[] args) + { + NSApplication.Init(); + NSApplication.SharedApplication.Delegate = new MauiMacOSApp(); + NSApplication.Main(args); + } +} +``` + +### MauiMacOSApp.cs — Application Delegate + +```csharp +// Platforms/macOS/MauiMacOSApp.cs +using Foundation; +using Microsoft.Maui.Platforms.MacOS.Hosting; + +namespace MyApp; + +[Register("MauiMacOSApp")] +public class MauiMacOSApp : MacOSMauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} +``` + +### MauiProgram.cs — App Builder + +```csharp +// Platforms/macOS/MauiProgram.cs +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platforms.MacOS.Hosting; + +namespace MyApp; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiAppMacOS() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + + return builder.Build(); + } +} +``` + +### App.cs — MAUI Application + +```csharp +// Platforms/macOS/App.cs +using Microsoft.Maui.Controls; + +namespace MyApp; + +public class MacOSApp : Application +{ + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new MainPage()); + } +} +``` + +Or with Shell navigation: + +```csharp +public class MacOSApp : Application +{ + protected override Window CreateWindow(IActivationState? activationState) + { + var shell = new Shell(); + + // Enable native macOS sidebar + MacOSShell.SetUseNativeSidebar(shell, true); + + // Add shell items + var main = new ShellContent + { + Title = "Home", + ContentTemplate = new DataTemplate(typeof(MainPage)), + Route = "home" + }; + shell.Items.Add(main); + + return new Window(shell); + } +} +``` + +## Step 4: Build and Run + +```bash +# Build +dotnet build MyApp.MacOS/MyApp.MacOS.csproj + +# The app bundle is created at: +# MyApp.MacOS/bin/Debug/net10.0-macos/osx-arm64/MyApp.app + +# Launch +open MyApp.MacOS/bin/Debug/net10.0-macos/osx-arm64/MyApp.app +``` + +## Conditional Compilation + +Use `#if` directives for macOS-specific code in shared files: + +```xml + + + $(DefineConstants);MACAPP + +``` + +```csharp +#if MACAPP +using Microsoft.Maui.Platforms.MacOS.Platform; + +// macOS-specific code +MacOSWindow.SetTitlebarStyle(window, MacOSTitlebarStyle.UnifiedCompact); +#endif +``` + +## Adding Platform-Specific Handlers + +Register custom handlers or override existing ones in `MauiProgram.cs`: + +```csharp +builder.ConfigureMauiHandlers(handlers => +{ + // Use native sidebar for FlyoutPage + handlers.AddHandler(); +}); +``` + +## Optional: Blazor Hybrid Support + +See [Blazor Hybrid](blazor-hybrid.md) for adding BlazorWebView support. + +## Optional: Essentials Support + +Add the Essentials package for macOS implementations of device APIs: + +```xml + +``` + +```csharp +// In MauiProgram.cs +builder.AddMacOSEssentials(); +``` + +## Tips + +- **SF Symbols**: Use SF Symbol names directly as `IconImageSource` values (e.g., `"gear"`, `"plus"`, `"square.and.arrow.up"`) +- **Window size**: Set initial window size via `Window.Width` and `Window.Height` in your `App.CreateWindow()` +- **Debug**: Use `open MyApp.app` to launch — the app runs independently of the terminal +- **Hot reload**: Not currently supported — rebuild and relaunch for changes +- **MainThread**: `MainThread.BeginInvokeOnMainThread()` is **not supported** — use `Dispatcher.Dispatch()` or `MainThreadHelper.BeginInvokeOnMainThread()` instead (see [Controls & Platform Notes](controls.md#dispatcher--threading)) diff --git a/platforms/MacOS/docs/guides/index.md b/platforms/MacOS/docs/guides/index.md new file mode 100644 index 000000000..2fbd1021c --- /dev/null +++ b/platforms/MacOS/docs/guides/index.md @@ -0,0 +1,59 @@ +# macOS (AppKit) Platform APIs + +This .NET MAUI backend provides native macOS experiences using AppKit. The following platform-specific APIs let you build apps that look and feel like first-class Mac citizens. + +## Guides + +| Topic | Description | +|-------|-------------| +| [Getting Started](getting-started.md) | Create a macOS app head project, link shared code & resources | +| [Sidebar Navigation](sidebar.md) | Native `NSSplitViewController` sidebar for Shell and FlyoutPage | +| [Toolbar](toolbar.md) | NSToolbar with native item types, placement, and layout | +| [Toolbar Item Types](toolbar-items.md) | Search, menu, segmented control, share, and popup toolbar items | +| [Window Configuration](window.md) | Titlebar style, transparency, and toolbar style | +| [Menu Bar](menu-bar.md) | Application menu bar and default menus | +| [Lifecycle Events](lifecycle.md) | App and window lifecycle events | +| [Theming](theming.md) | Light/dark mode and appearance | +| [Blazor Hybrid](blazor-hybrid.md) | BlazorWebView with WKWebView | +| [Controls & Platform Notes](controls.md) | MapView, gestures, modals, icons, fonts, threading | + +## Quick Start + +Register the macOS backend in your app: + +```csharp +// MauiProgram.cs +builder.UseMauiApp() + .UseMacOS(); +``` + +### Native Sidebar with Shell + +```csharp +var shell = new Shell(); +MacOSShell.SetUseNativeSidebar(shell, true); +``` + +### Toolbar Items + +```csharp +// Add a toolbar item to the sidebar area +var item = new ToolbarItem { Text = "Refresh", IconImageSource = "arrow.clockwise" }; +MacOSToolbarItem.SetPlacement(item, MacOSToolbarItemPlacement.SidebarLeading); +page.ToolbarItems.Add(item); +``` + +### Window Titlebar + +```csharp +MacOSWindow.SetTitlebarStyle(window, MacOSTitlebarStyle.UnifiedCompact); +MacOSWindow.SetTitlebarTransparent(window, true); +``` + +## Platform-Specific Namespace + +All macOS APIs are in the `Microsoft.Maui.Platforms.MacOS.Platform` namespace: + +```csharp +using Microsoft.Maui.Platforms.MacOS.Platform; +``` diff --git a/platforms/MacOS/docs/guides/lifecycle.md b/platforms/MacOS/docs/guides/lifecycle.md new file mode 100644 index 000000000..e72a59ec9 --- /dev/null +++ b/platforms/MacOS/docs/guides/lifecycle.md @@ -0,0 +1,97 @@ +# Lifecycle Events + +Subscribe to macOS application lifecycle events for state management, analytics, and cleanup. + +## Available Events + +| Event | When it Fires | +|-------|---------------| +| `DidFinishLaunching` | App has finished launching and is ready | +| `DidBecomeActive` | App (or a window) gained focus | +| `DidResignActive` | App (or a window) lost focus | +| `DidHide` | App was hidden (⌘H or Hide menu) | +| `DidUnhide` | App was unhidden / shown again | +| `WillTerminate` | App is about to quit | + +## Subscribing to Events + +Register lifecycle handlers in `MauiProgram.cs`: + +```csharp +// MauiProgram.cs +builder.ConfigureLifecycleEvents(events => +{ + events.AddMacOS(mac => + { + mac.DidFinishLaunching((notification) => + { + Console.WriteLine("App launched"); + }); + + mac.DidBecomeActive((notification) => + { + Console.WriteLine("App became active"); + }); + + mac.DidResignActive((notification) => + { + Console.WriteLine("App resigned active"); + }); + + mac.DidHide((notification) => + { + Console.WriteLine("App hidden"); + }); + + mac.DidUnhide((notification) => + { + Console.WriteLine("App shown"); + }); + + mac.WillTerminate((notification) => + { + Console.WriteLine("App terminating — save state"); + }); + }); +}); +``` + +## MAUI Application Lifecycle + +The standard MAUI `Application` lifecycle methods also work: + +```csharp +public class MacOSApp : Application +{ + protected override Window CreateWindow(IActivationState? activationState) + { + var window = new Window(new MainPage()); + + window.Created += (s, e) => Console.WriteLine("Window created"); + window.Activated += (s, e) => Console.WriteLine("Window activated"); + window.Deactivated += (s, e) => Console.WriteLine("Window deactivated"); + window.Stopped += (s, e) => Console.WriteLine("Window stopped"); + window.Resumed += (s, e) => Console.WriteLine("Window resumed"); + window.Destroying += (s, e) => Console.WriteLine("Window destroying"); + + return window; + } +} +``` + +## Window Close Button + +When the user clicks the red close button (traffic light), the platform fires `IWindow.Destroying()` via a `MacOSWindowDelegate`. This allows you to save state or prompt the user before the window closes. + +## Event Flow + +Typical lifecycle sequence: + +``` +App start: DidFinishLaunching → DidBecomeActive +Switch away: DidResignActive +Switch back: DidBecomeActive +Hide (⌘H): DidResignActive → DidHide +Unhide: DidUnhide → DidBecomeActive +Quit (⌘Q): DidResignActive → WillTerminate +``` diff --git a/platforms/MacOS/docs/guides/menu-bar.md b/platforms/MacOS/docs/guides/menu-bar.md new file mode 100644 index 000000000..cf209e985 --- /dev/null +++ b/platforms/MacOS/docs/guides/menu-bar.md @@ -0,0 +1,88 @@ +# Menu Bar + +Configure the native macOS application menu bar. + +## Default Menus + +The platform automatically creates standard macOS menus: + +- **App menu** — About, Preferences, Quit (with ⌘Q) +- **Edit menu** — Undo, Redo, Cut, Copy, Paste, Delete, Select All +- **Window menu** — Minimize, Zoom, Full Screen + +### Customizing Defaults + +Control which default menus are included via `ConfigureMacOSMenuBar()`: + +```csharp +// MauiProgram.cs +builder.ConfigureMacOSMenuBar(options => +{ + options.IncludeDefaultMenus = true; // Master toggle (default: true) + options.IncludeDefaultEditMenu = true; // Edit menu (default: true) + options.IncludeDefaultWindowMenu = true; // Window menu (default: true) +}); +``` + +Setting `IncludeDefaultMenus = false` disables all default menus. You can then build the entire menu bar from scratch using `Page.MenuBarItems`. + +## Page-Level Menu Items + +Add custom menus via `Page.MenuBarItems` on any `ContentPage`: + +```csharp +public class MyPage : ContentPage +{ + public MyPage() + { + // Add a "File" menu + var fileMenu = new MenuBarItem { Text = "File" }; + + fileMenu.Add(new MenuFlyoutItem + { + Text = "New", + Command = new Command(() => CreateNew()), + KeyboardAccelerators = + { + new KeyboardAccelerator { Modifiers = KeyboardAcceleratorModifiers.Cmd, Key = "n" } + } + }); + + fileMenu.Add(new MenuFlyoutItem + { + Text = "Open...", + Command = new Command(() => OpenFile()), + KeyboardAccelerators = + { + new KeyboardAccelerator { Modifiers = KeyboardAcceleratorModifiers.Cmd, Key = "o" } + } + }); + + fileMenu.Add(new MenuFlyoutSeparator()); + + fileMenu.Add(new MenuFlyoutItem + { + Text = "Save", + Command = new Command(() => Save()), + KeyboardAccelerators = + { + new KeyboardAccelerator { Modifiers = KeyboardAcceleratorModifiers.Cmd, Key = "s" } + } + }); + + MenuBarItems.Add(fileMenu); + } +} +``` + +Menu bar items are merged with the default menus. Page-level menus update automatically when the active page changes. + +## API Reference + +### MacOSMenuBarOptions + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `IncludeDefaultMenus` | `bool` | `true` | Include any default menus | +| `IncludeDefaultEditMenu` | `bool` | `true` | Include Edit menu (Undo, Copy, Paste, etc.) | +| `IncludeDefaultWindowMenu` | `bool` | `true` | Include Window menu (Minimize, Zoom, etc.) | diff --git a/platforms/MacOS/docs/guides/sidebar.md b/platforms/MacOS/docs/guides/sidebar.md new file mode 100644 index 000000000..7d4e757c3 --- /dev/null +++ b/platforms/MacOS/docs/guides/sidebar.md @@ -0,0 +1,163 @@ +# Sidebar Navigation + +Build native macOS sidebar navigation using `NSSplitViewController` with behind-window vibrancy, SF Symbol icons, and structured source-list groups. + +## Setup + +There are two sidebar implementations: **Shell sidebar** and **FlyoutPage sidebar**. Both use `NSSplitViewController` with `NSOutlineView` source-list styling. + +### Shell Sidebar + +Enable the native sidebar on a `Shell`: + +```csharp +var shell = new Shell(); +MacOSShell.SetUseNativeSidebar(shell, true); +``` + +`UseNativeSidebar` is an opt-in flag. When `true`, the `ShellHandler` creates an `NSOutlineView` with source-list styling and behind-window vibrancy. When `false` (default), the Shell uses a standard MAUI flyout layout. + +### FlyoutPage Sidebar + +For `FlyoutPage`, you must **register the handler** and **opt in** to the native sidebar: + +```csharp +// MauiProgram.cs — register the handler (required) +builder.ConfigureMauiHandlers(handlers => +{ + handlers.AddHandler(); +}); +``` + +```csharp +// Page code — opt in to NSSplitViewController sidebar (required) +var flyoutPage = new FlyoutPage(); +MacOSFlyoutPage.SetUseNativeSidebar(flyoutPage, true); +``` + +> **Important:** Both steps are required for `FlyoutPage`: +> - **Handler registration** replaces the default `FlyoutPageHandler` with `NativeSidebarFlyoutPageHandler` +> - **`UseNativeSidebar = true`** opts the specific `FlyoutPage` instance into `NSSplitViewController` with sidebar vibrancy and traffic lights inside the sidebar +> - Without `UseNativeSidebar`, the handler uses a plain `NSSplitView` without source-list styling + +The sidebar renders `ShellItem` and `ShellSection` entries in an `NSOutlineView` source list. Sections with multiple `ShellContent` children appear as expandable groups. + +### SF Symbol Icons + +Assign SF Symbol names to shell items for native icons: + +```csharp +var section = new ShellSection { Title = "Settings" }; +MacOSShell.SetSystemImage(section, "gear"); +``` + +Works on `ShellItem`, `ShellSection`, and `ShellContent`. + +### Sidebar Resizing + +By default the sidebar can be resized by dragging. To lock it: + +```csharp +MacOSShell.SetIsSidebarResizable(shell, false); +``` + +### Sidebar Width + +Set the sidebar width via `Shell.FlyoutWidth`: + +```csharp +shell.FlyoutWidth = 280; +``` + +The default width is 300px. The minimum is 150px and maximum is 400px. + +## FlyoutPage Sidebar + +`FlyoutPage` can also use the native sidebar appearance: + +```csharp +var flyoutPage = new FlyoutPage(); +MacOSFlyoutPage.SetUseNativeSidebar(flyoutPage, true); +``` + +### Structured Sidebar Items + +Provide structured items with groups, icons, and hierarchy: + +```csharp +var items = new List +{ + new MacOSSidebarItem + { + Title = "Library", + Children = new List + { + new MacOSSidebarItem { Title = "Music", SystemImage = "music.note" }, + new MacOSSidebarItem { Title = "Movies", SystemImage = "film" }, + new MacOSSidebarItem { Title = "Podcasts", SystemImage = "mic" }, + } + }, + new MacOSSidebarItem + { + Title = "Playlists", + Children = new List + { + new MacOSSidebarItem { Title = "Favorites", SystemImage = "heart" }, + new MacOSSidebarItem { Title = "Recently Added", SystemImage = "clock" }, + } + } +}; + +MacOSFlyoutPage.SetSidebarItems(flyoutPage, items); +``` + +Items with `Children` become group headers (non-selectable, bold uppercase text). Leaf items are selectable rows. + +### Selection Handling + +```csharp +MacOSFlyoutPage.SetSidebarSelectionChanged(flyoutPage, item => +{ + Console.WriteLine($"Selected: {item.Title}"); + // Navigate to the appropriate detail page + flyoutPage.Detail = new NavigationPage(GetPageForItem(item)); +}); +``` + +### Programmatic Selection + +```csharp +MacOSFlyoutPage.SetSelectedItem(flyoutPage, items[0].Children[1]); +``` + +> **Note:** Programmatic selection via `SetSelectedItem` or `SelectSidebarItem` does **not** trigger the `SidebarSelectionChanged` callback. This is intentional — it prevents infinite loops when you update the detail page in the selection callback, which would otherwise re-trigger the callback. If you need to respond to programmatic selections, update your UI directly after calling `SetSelectedItem`. + +## MacOSSidebarItem + +| Property | Type | Description | +|----------|------|-------------| +| `Title` | `string` | Display text | +| `SystemImage` | `string?` | SF Symbol name for the icon | +| `Icon` | `ImageSource?` | Custom icon image | +| `Children` | `List?` | Child items (makes this a group header) | +| `Tag` | `string?` | Identifier for programmatic lookup | +| `IsGroup` | `bool` | Read-only; `true` when `Children` is non-empty | + +## API Reference + +### MacOSShell (Attached Properties) + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `UseNativeSidebar` | `bool` | `false` | Use `NSSplitViewController` for sidebar | +| `SystemImage` | `string?` | `null` | SF Symbol name for sidebar icon | +| `IsSidebarResizable` | `bool` | `true` | Allow user to resize sidebar | + +### MacOSFlyoutPage (Attached Properties) + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `UseNativeSidebar` | `bool` | `false` | Use `NSSplitViewController` for sidebar | +| `SidebarItems` | `IList?` | `null` | Structured sidebar items | +| `SidebarSelectionChanged` | `Action?` | `null` | Selection callback | +| `SelectedItem` | `MacOSSidebarItem?` | `null` | Currently selected item | diff --git a/platforms/MacOS/docs/guides/theming.md b/platforms/MacOS/docs/guides/theming.md new file mode 100644 index 000000000..0f9333e57 --- /dev/null +++ b/platforms/MacOS/docs/guides/theming.md @@ -0,0 +1,56 @@ +# Theming + +Light and dark mode support with automatic theme change detection. + +## Setting the App Theme + +Set the application theme programmatically: + +```csharp +Application.Current.UserAppTheme = AppTheme.Dark; // Force dark +Application.Current.UserAppTheme = AppTheme.Light; // Force light +Application.Current.UserAppTheme = AppTheme.Unspecified; // Follow system +``` + +The platform maps MAUI themes to macOS appearances: + +| `AppTheme` | `NSAppearance` | +|------------|----------------| +| `Light` | `NSAppearance.NameAqua` | +| `Dark` | `NSAppearance.NameDarkAqua` | +| `Unspecified` | System setting (follows macOS Appearance preference) | + +## Responding to Theme Changes + +When macOS switches between light and dark mode (via System Settings or auto-schedule), the platform automatically detects the change and fires `Application.RequestedThemeChanged`: + +```csharp +Application.Current.RequestedThemeChanged += (s, e) => +{ + Console.WriteLine($"Theme changed to: {e.RequestedTheme}"); +}; +``` + +### AppThemeBinding in XAML + +Use `AppThemeBinding` for theme-aware resources (works the same as iOS/Android): + +```xml +