diff --git a/.gitignore b/.gitignore index ca54461..7451049 100644 --- a/.gitignore +++ b/.gitignore @@ -352,4 +352,5 @@ MigrationBackup/ .idea Resource.designer.cs -nupkgs/ \ No newline at end of file +nupkgs/ +.DS_Store diff --git a/Fabulous.MauiControls.sln b/Fabulous.MauiControls.sln index 730e272..9f63990 100644 --- a/Fabulous.MauiControls.sln +++ b/Fabulous.MauiControls.sln @@ -34,6 +34,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Files", "_Solutio EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gallery", "samples\Gallery\Gallery.fsproj", "{A28D6852-F21C-4A43-93AF-CC71050028A9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Navigation", "Navigation", "{3A3581BD-4228-49B0-84D5-AF39D620BA34}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BasicNavigation", "samples\Navigation\BasicNavigation\BasicNavigation.fsproj", "{CE61493B-86CC-49CE-9443-F25F1ECB15C9}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NavigationPath", "samples\Navigation\NavigationPath\NavigationPath.fsproj", "{5B3F6C4E-82CF-442F-BFB4-216C1CD85700}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +87,14 @@ Global {A28D6852-F21C-4A43-93AF-CC71050028A9}.Release|Any CPU.Build.0 = Release|Any CPU {A28D6852-F21C-4A43-93AF-CC71050028A9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {A28D6852-F21C-4A43-93AF-CC71050028A9}.Release|Any CPU.Deploy.0 = Release|Any CPU + {CE61493B-86CC-49CE-9443-F25F1ECB15C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE61493B-86CC-49CE-9443-F25F1ECB15C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE61493B-86CC-49CE-9443-F25F1ECB15C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE61493B-86CC-49CE-9443-F25F1ECB15C9}.Release|Any CPU.Build.0 = Release|Any CPU + {5B3F6C4E-82CF-442F-BFB4-216C1CD85700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B3F6C4E-82CF-442F-BFB4-216C1CD85700}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B3F6C4E-82CF-442F-BFB4-216C1CD85700}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B3F6C4E-82CF-442F-BFB4-216C1CD85700}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {67FB01A1-1A3E-4A3B-83DC-7D63B56FB1A1} = {35A6823C-8312-4F92-818A-5117BB31A569} @@ -90,5 +104,8 @@ Global {BD278E92-A806-4913-AB2D-59D59FE8E2AD} = {87C8E9E8-497E-46DB-90FE-4402E0CB230A} {97FE743C-512F-44BA-92CF-3EB3A7972E5D} = {87C8E9E8-497E-46DB-90FE-4402E0CB230A} {A28D6852-F21C-4A43-93AF-CC71050028A9} = {87C8E9E8-497E-46DB-90FE-4402E0CB230A} + {3A3581BD-4228-49B0-84D5-AF39D620BA34} = {87C8E9E8-497E-46DB-90FE-4402E0CB230A} + {CE61493B-86CC-49CE-9443-F25F1ECB15C9} = {3A3581BD-4228-49B0-84D5-AF39D620BA34} + {5B3F6C4E-82CF-442F-BFB4-216C1CD85700} = {3A3581BD-4228-49B0-84D5-AF39D620BA34} EndGlobalSection EndGlobal diff --git a/samples/Navigation/BasicNavigation/BasicNavigation.fsproj b/samples/Navigation/BasicNavigation/BasicNavigation.fsproj new file mode 100644 index 0000000..3fd59ae --- /dev/null +++ b/samples/Navigation/BasicNavigation/BasicNavigation.fsproj @@ -0,0 +1,105 @@ + + + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + + + Exe + NavigationSample + true + true + false + + + NavigationSample + + + org.fabulous.maui.basicnavigation + c6c98ddf-a204-4a7e-be03-2829f110f396 + + + 1.0 + 1 + + $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/BasicNavigation/MauiProgram.fs b/samples/Navigation/BasicNavigation/MauiProgram.fs new file mode 100644 index 0000000..9365d83 --- /dev/null +++ b/samples/Navigation/BasicNavigation/MauiProgram.fs @@ -0,0 +1,16 @@ +namespace NavigationSample + +open Microsoft.Maui.Hosting +open Fabulous.Maui + +type MauiProgram = + static member CreateMauiApp() = + MauiApp + .CreateBuilder() + .UseFabulousApp(Sample.program) + .ConfigureFonts(fun fonts -> + fonts + .AddFont("OpenSans-Regular.ttf", "OpenSansRegular") + .AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold") + |> ignore) + .Build() diff --git a/samples/Navigation/BasicNavigation/PageA.fs b/samples/Navigation/BasicNavigation/PageA.fs new file mode 100644 index 0000000..05b2842 --- /dev/null +++ b/samples/Navigation/BasicNavigation/PageA.fs @@ -0,0 +1,29 @@ +namespace NavigationSample + +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageA = + type Model = { Count: int } + + type Msg = + | Increment + | Decrement + + let init () = { Count = 0 } + + let update msg model = + match msg with + | Increment -> { Count = model.Count + 1 } + | Decrement -> { Count = model.Count - 1 } + + let view model = + VStack() { + Label("Page A").font(32.).centerTextHorizontal().margin(0., 0., 0., 30.) + + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } diff --git a/samples/Navigation/BasicNavigation/PageB.fs b/samples/Navigation/BasicNavigation/PageB.fs new file mode 100644 index 0000000..61a8f4f --- /dev/null +++ b/samples/Navigation/BasicNavigation/PageB.fs @@ -0,0 +1,29 @@ +namespace NavigationSample + +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageB = + type Model = { Count: int } + + type Msg = + | Increment + | Decrement + + let init () = { Count = 0 } + + let update msg model = + match msg with + | Increment -> { Count = model.Count + 1 } + | Decrement -> { Count = model.Count - 1 } + + let view model = + VStack() { + Label("Page B").font(32.).centerTextHorizontal().margin(0., 0., 0., 30.) + + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } diff --git a/samples/Navigation/BasicNavigation/PageC.fs b/samples/Navigation/BasicNavigation/PageC.fs new file mode 100644 index 0000000..0503667 --- /dev/null +++ b/samples/Navigation/BasicNavigation/PageC.fs @@ -0,0 +1,29 @@ +namespace NavigationSample + +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageC = + type Model = { Count: int } + + type Msg = + | Increment + | Decrement + + let init () = { Count = 0 } + + let update msg model = + match msg with + | Increment -> { Count = model.Count + 1 } + | Decrement -> { Count = model.Count - 1 } + + let view model = + VStack() { + Label("Page C").font(32.).centerTextHorizontal().margin(0., 0., 0., 30.) + + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } diff --git a/samples/Navigation/BasicNavigation/Platforms/Android/AndroidManifest.xml b/samples/Navigation/BasicNavigation/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..bdec9b5 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Platforms/Android/MainActivity.fs b/samples/Navigation/BasicNavigation/Platforms/Android/MainActivity.fs new file mode 100644 index 0000000..85f15ab --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Android/MainActivity.fs @@ -0,0 +1,17 @@ +namespace NavigationSample + +open Android.App +open Android.Content.PM +open Microsoft.Maui + +[] +type MainActivity() = + inherit MauiAppCompatActivity() diff --git a/samples/Navigation/BasicNavigation/Platforms/Android/MainApplication.fs b/samples/Navigation/BasicNavigation/Platforms/Android/MainApplication.fs new file mode 100644 index 0000000..7910a54 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Android/MainApplication.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Android.App +open Microsoft.Maui + +[] +type MainApplication(handle, ownership) = + inherit MauiApplication(handle, ownership) + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/BasicNavigation/Platforms/Android/Resources/values/colors.xml b/samples/Navigation/BasicNavigation/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..5cd1604 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/AppDelegate.fs b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/AppDelegate.fs new file mode 100644 index 0000000..90458e5 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override this.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Info.plist b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..0690e47 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Program.fs b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Program.fs new file mode 100644 index 0000000..26c87e7 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/MacCatalyst/Program.fs @@ -0,0 +1,9 @@ +namespace NavigationSample + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/BasicNavigation/Platforms/Tizen/Main.fs b/samples/Navigation/BasicNavigation/Platforms/Tizen/Main.fs new file mode 100644 index 0000000..3d59efd --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Tizen/Main.fs @@ -0,0 +1,16 @@ +namespace NavigationSample + +open System +open Microsoft.Maui +open Microsoft.Maui.Hosting + +type Program() = + inherit MauiApplication() + + override this.CreateMauiApp() = MauiProgram.CreateMauiApp() + +module Program = + [] + let main args = + let app = Program() + app.Run(args) diff --git a/samples/Navigation/BasicNavigation/Platforms/Tizen/tizen-manifest.xml b/samples/Navigation/BasicNavigation/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..83e2649 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + appicon.xhigh.png + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Platforms/Windows/App.fs b/samples/Navigation/BasicNavigation/Platforms/Windows/App.fs new file mode 100644 index 0000000..ebceb21 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Windows/App.fs @@ -0,0 +1,10 @@ +namespace NavigationSample.WinUI + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +type App() = + inherit FSharp.Maui.WinUICompat.App() + + override this.CreateMauiApp() = + NavigationSample.MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/BasicNavigation/Platforms/Windows/Main.fs b/samples/Navigation/BasicNavigation/Platforms/Windows/Main.fs new file mode 100644 index 0000000..501c15d --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Windows/Main.fs @@ -0,0 +1,9 @@ +namespace NavigationSample.WinUI + +open System + +module Program = + [] + let main args = + do FSharp.Maui.WinUICompat.Program.Main(args, typeof) + 0 diff --git a/samples/Navigation/BasicNavigation/Platforms/Windows/Package.appxmanifest b/samples/Navigation/BasicNavigation/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..e98c6ed --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,43 @@ + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/BasicNavigation/Platforms/Windows/app.manifest b/samples/Navigation/BasicNavigation/Platforms/Windows/app.manifest new file mode 100644 index 0000000..c8a173c --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/samples/Navigation/BasicNavigation/Platforms/iOS/AppDelegate.fs b/samples/Navigation/BasicNavigation/Platforms/iOS/AppDelegate.fs new file mode 100644 index 0000000..7418834 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/iOS/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/BasicNavigation/Platforms/iOS/Info.plist b/samples/Navigation/BasicNavigation/Platforms/iOS/Info.plist new file mode 100644 index 0000000..358337b --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Navigation/BasicNavigation/Platforms/iOS/Program.fs b/samples/Navigation/BasicNavigation/Platforms/iOS/Program.fs new file mode 100644 index 0000000..26c87e7 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Platforms/iOS/Program.fs @@ -0,0 +1,9 @@ +namespace NavigationSample + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/BasicNavigation/Properties/launchSettings.json b/samples/Navigation/BasicNavigation/Properties/launchSettings.json new file mode 100644 index 0000000..c16206a --- /dev/null +++ b/samples/Navigation/BasicNavigation/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Resources/AppIcon/appicon.svg b/samples/Navigation/BasicNavigation/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..5f04fcf --- /dev/null +++ b/samples/Navigation/BasicNavigation/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Resources/AppIcon/appiconfg.svg b/samples/Navigation/BasicNavigation/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Regular.ttf b/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..534d009 Binary files /dev/null and b/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Semibold.ttf b/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..f333153 Binary files /dev/null and b/samples/Navigation/BasicNavigation/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/samples/Navigation/BasicNavigation/Resources/Images/dotnet_bot.svg b/samples/Navigation/BasicNavigation/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..51b1c33 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/BasicNavigation/Resources/Raw/AboutAssets.txt b/samples/Navigation/BasicNavigation/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..50b8a7b --- /dev/null +++ b/samples/Navigation/BasicNavigation/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/samples/Navigation/BasicNavigation/Resources/Splash/splash.svg b/samples/Navigation/BasicNavigation/Resources/Splash/splash.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/BasicNavigation/Sample.fs b/samples/Navigation/BasicNavigation/Sample.fs new file mode 100644 index 0000000..2b8ba96 --- /dev/null +++ b/samples/Navigation/BasicNavigation/Sample.fs @@ -0,0 +1,80 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// The most basic navigation with Fabulous is done by swapping widgets. +/// +/// In this sample, we have three pages, and we swap between them by changing the "current step" (represented by a discriminated union). +/// All three pages' models are loaded at the same time, but only the current page is rendered. +/// +/// NOTE: This approach is not using any official navigation system. +/// Hence there is no back button, no navigation history, no animations, etc by default. +/// Fabulous will only give the illusion of navigation by swapping the pages. +module Sample = + [] + type Step = + | PageA + | PageB + | PageC + + type Model = + { CurrentStep: Step + PageAModel: PageA.Model + PageBModel: PageB.Model + PageCModel: PageC.Model } + + type Msg = + | PageAMsg of PageA.Msg + | PageBMsg of PageB.Msg + | PageCMsg of PageC.Msg + | GoToPageA + | GoToPageB + | GoToPageC + + let init () = + { CurrentStep = Step.PageA + PageAModel = PageA.init() + PageBModel = PageB.init() + PageCModel = PageC.init() } + + let update msg model = + match msg with + | PageAMsg msg -> + { model with + PageAModel = PageA.update msg model.PageAModel } + | PageBMsg msg -> + { model with + PageBModel = PageB.update msg model.PageBModel } + | PageCMsg msg -> + { model with + PageCModel = PageC.update msg model.PageCModel } + | GoToPageA -> { model with CurrentStep = Step.PageA } + | GoToPageB -> { model with CurrentStep = Step.PageB } + | GoToPageC -> { model with CurrentStep = Step.PageC } + + let view model = + Application( + ContentPage( + (Grid(coldefs = [ Star; Star; Star ], rowdefs = [ Auto; Star ]) { + Button("Page A", GoToPageA).gridColumn(0) + + Button("Page B", GoToPageB).gridColumn(1) + + Button("Page C", GoToPageC).gridColumn(2) + + + (match model.CurrentStep with + | Step.PageA -> View.map PageAMsg (PageA.view model.PageAModel) + | Step.PageB -> View.map PageBMsg (PageB.view model.PageBModel) + | Step.PageC -> View.map PageCMsg (PageC.view model.PageCModel)) + .gridRow(1) + .gridColumnSpan(3) + }) + .rowSpacing(30.) + ) + ) + + let program = Program.stateful init update view diff --git a/samples/Navigation/NavigationPath/MauiProgram.fs b/samples/Navigation/NavigationPath/MauiProgram.fs new file mode 100644 index 0000000..9365d83 --- /dev/null +++ b/samples/Navigation/NavigationPath/MauiProgram.fs @@ -0,0 +1,16 @@ +namespace NavigationSample + +open Microsoft.Maui.Hosting +open Fabulous.Maui + +type MauiProgram = + static member CreateMauiApp() = + MauiApp + .CreateBuilder() + .UseFabulousApp(Sample.program) + .ConfigureFonts(fun fonts -> + fonts + .AddFont("OpenSans-Regular.ttf", "OpenSansRegular") + .AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold") + |> ignore) + .Build() diff --git a/samples/Navigation/NavigationPath/NavigationPath.fsproj b/samples/Navigation/NavigationPath/NavigationPath.fsproj new file mode 100644 index 0000000..603c0a9 --- /dev/null +++ b/samples/Navigation/NavigationPath/NavigationPath.fsproj @@ -0,0 +1,107 @@ + + + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + + + Exe + NavigationSample + true + true + false + + + NavigationSample + + + org.fabulous.maui.basicnavigation + 95540b31-0bdf-4c31-a775-610721869c08 + + + 1.0 + 1 + + $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/NavigationPath/NavigationState.fs b/samples/Navigation/NavigationPath/NavigationState.fs new file mode 100644 index 0000000..2c237ff --- /dev/null +++ b/samples/Navigation/NavigationPath/NavigationState.fs @@ -0,0 +1,83 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// MVU is a very explicit but also very verbose pattern. +/// Everything needs to be explicitly written out. +/// +/// This NavigationState file acts as the glue between the different pages. +/// Relying on the NavigationPath DU, we call the init, update and view functions of the different pages. +/// Like this, everything is centralized and the root of the app doesn't have to know about the other pages. +module NavigationState = + type Model = + | PageAModel of PageA.Model + | PageBModel of PageB.Model + | PageCModel of PageC.Model + + type Msg = + | PageAMsg of PageA.Msg + | PageBMsg of PageB.Msg + | PageCMsg of PageC.Msg + + let init path = + match path with + | NavigationPath.PageA -> PageAModel(PageA.init()) + | NavigationPath.PageB initialCount -> PageBModel(PageB.init initialCount) + | NavigationPath.PageC(someArgs, stepCount) -> PageCModel(PageC.init someArgs stepCount) + + let update nav (msg: Msg) (model: Model) = + match msg, model with + | PageAMsg msg, PageAModel model -> + let m, c = PageA.update nav msg model + PageAModel m, Cmd.map PageAMsg c + + | PageBMsg msg, PageBModel model -> + let m, c = PageB.update nav msg model + PageBModel m, Cmd.map PageBMsg c + + | PageCMsg msg, PageCModel model -> + let m, c = PageC.update nav msg model + PageCModel m, Cmd.map PageCMsg c + + | _ -> model, Cmd.none + + let view model = + match model with + | PageAModel model -> AnyPage(View.map PageAMsg (PageA.view model)) + | PageBModel model -> AnyPage(View.map PageBMsg (PageB.view model)) + | PageCModel model -> AnyPage(View.map PageCMsg (PageC.view model)) + + let updateBackButton nav model = + match model with + | PageAModel model -> update nav (PageAMsg PageA.BackButtonPressed) (PageAModel model) + | _ -> model, Cmd.none + +/// The NavigationStack represents the history of the navigation. +/// This is a simple stack of pages that the app will use to remember and display the pages needed. +type NavigationStack = + { BackStack: NavigationState.Model list + CurrentPage: NavigationState.Model + ForwardStack: NavigationState.Model list } + + static member Init(model: NavigationState.Model) = + { BackStack = [] + CurrentPage = model + ForwardStack = [] } + + member this.Push(model: NavigationState.Model) = + { BackStack = this.CurrentPage :: this.BackStack + CurrentPage = model + ForwardStack = [] } + + member this.Pop() = + match this.BackStack with + | [] -> this + | head :: tail -> + { BackStack = tail + CurrentPage = head + ForwardStack = [] } + + member this.UpdateCurrentPage(newPage: NavigationState.Model) = { this with CurrentPage = newPage } diff --git a/samples/Navigation/NavigationPath/NavigationTypes.fs b/samples/Navigation/NavigationPath/NavigationTypes.fs new file mode 100644 index 0000000..241d6d8 --- /dev/null +++ b/samples/Navigation/NavigationPath/NavigationTypes.fs @@ -0,0 +1,39 @@ +namespace NavigationSample + +open Fabulous + +/// This is the centerpiece of navigating through paths: +/// A single enum regrouping all the navigation routes with their arguments +[] +type NavigationPath = + | PageA + | PageB of initialCount: int + | PageC of someArgs: string * stepCount: int + +/// The NavigationController is used to notify the intention to navigate to a new page (or go back). +/// We listen to it in a Cmd that will dispatch a message to the root of the application to trigger the actual navigation. +type NavigationController() = + let navigated = Event() + let backNavigated = Event() + + member this.Navigated = navigated.Publish + member this.BackNavigated = backNavigated.Publish + + member this.NavigateTo(path: NavigationPath) = navigated.Trigger(path) + + member this.NavigateBack() = backNavigated.Trigger() + +/// The Navigation module is a set of helper functions that will wrap the call to NavigationController into a Cmd. +/// We do that because navigation is a side-effect and we want to keep it in a Cmd. +module Navigation = + let private navigateTo (nav: NavigationController) path : Cmd<'msg> = [ fun _ -> nav.NavigateTo(path) ] + + let navigateBack (nav: NavigationController) : Cmd<'msg> = [ fun _ -> nav.NavigateBack() ] + + let navigateToPageA nav = navigateTo nav NavigationPath.PageA + + let navigateToPageB nav initialCount = + navigateTo nav (NavigationPath.PageB initialCount) + + let navigateToPageC nav someArgs stepCount = + navigateTo nav (NavigationPath.PageC(someArgs, stepCount)) diff --git a/samples/Navigation/NavigationPath/PageA.fs b/samples/Navigation/NavigationPath/PageA.fs new file mode 100644 index 0000000..89bd3a5 --- /dev/null +++ b/samples/Navigation/NavigationPath/PageA.fs @@ -0,0 +1,51 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// Each pages are "isolated". They have their own MVU loop and own types. +/// The only dependency they receive from outside is the NavigationController, which is passed to the update function. +module PageA = + type Model = { Count: int } + + type Msg = + | Increment + | Decrement + | GoBack + | GoToPageB + | GoToPageC + | BackButtonPressed + + /// Since the NavigationPath.PageA doesn't take arguments, the init function excepts a unit parameter. + let init () = { Count = 0 } + + let update (nav: NavigationController) msg model = + match msg with + | Increment -> { Count = model.Count + 1 }, Cmd.none + | Decrement -> { Count = model.Count - 1 }, Cmd.none + | GoBack -> model, Navigation.navigateBack nav + | GoToPageB -> model, Navigation.navigateToPageB nav model.Count + | GoToPageC -> model, Navigation.navigateToPageC nav "Hello from Page A!" model.Count + | BackButtonPressed -> { Count = model.Count - 1 }, Cmd.none + + let view model = + ContentPage( + Grid(coldefs = [ Star ], rowdefs = [ Star; Auto ]) { + VStack() { + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } + + (VStack() { + Button("Go back", GoBack) + Button("Go to Page B", GoToPageB) + Button("Go to Page C", GoToPageC) + }) + .gridRow(1) + } + ) + .title("Page A") diff --git a/samples/Navigation/NavigationPath/PageB.fs b/samples/Navigation/NavigationPath/PageB.fs new file mode 100644 index 0000000..6ca5275 --- /dev/null +++ b/samples/Navigation/NavigationPath/PageB.fs @@ -0,0 +1,51 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageB = + type Model = { InitialCount: int; Count: int } + + type Msg = + | Increment + | Decrement + | GoBack + | GoToPageA + | GoToPageC + + /// Contrary to PageA, NavigationPath.PageB has a initialCount argument so the init function will receive it. + let init initialCount = + { InitialCount = initialCount + Count = initialCount } + + let update (nav: NavigationController) msg model = + match msg with + | Increment -> { model with Count = model.Count + 1 }, Cmd.none + | Decrement -> { model with Count = model.Count - 1 }, Cmd.none + | GoBack -> model, Navigation.navigateBack nav + | GoToPageA -> model, Navigation.navigateToPageA nav + | GoToPageC -> model, Navigation.navigateToPageC nav "Hello from Page A!" model.Count + + let view model = + ContentPage( + Grid(coldefs = [ Star ], rowdefs = [ Star; Auto ]) { + VStack() { + Label($"Initial count: {model.InitialCount}") + + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } + + (VStack() { + Button("Go back", GoBack) + Button("Go to Page A", GoToPageA) + Button("Go to Page C", GoToPageC) + }) + .gridRow(1) + } + ) + .title("Page B") diff --git a/samples/Navigation/NavigationPath/PageC.fs b/samples/Navigation/NavigationPath/PageC.fs new file mode 100644 index 0000000..2004ac0 --- /dev/null +++ b/samples/Navigation/NavigationPath/PageC.fs @@ -0,0 +1,61 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageC = + type Model = + { Args: string + StepCount: int + Count: int } + + type Msg = + | Increment + | Decrement + | GoBack + | GoToPageA + | GoToPageB + + let init args stepCount = + { Args = args + StepCount = stepCount + Count = 0 } + + let update (nav: NavigationController) msg model = + match msg with + | Increment -> + { model with + Count = model.Count + model.StepCount }, + Cmd.none + | Decrement -> + { model with + Count = model.Count - model.StepCount }, + Cmd.none + | GoBack -> model, Navigation.navigateBack nav + | GoToPageA -> model, Navigation.navigateToPageA nav + | GoToPageB -> model, Navigation.navigateToPageB nav model.Count + + let view model = + ContentPage( + Grid(coldefs = [ Star ], rowdefs = [ Star; Auto ]) { + VStack() { + Label($"Args: {model.Args}") + Label($"StepCount from Page B: {model.StepCount}") + + Label($"Count: {model.Count}").centerTextHorizontal() + + Button("Increment", Increment) + Button("Decrement", Decrement) + } + + (VStack() { + Button("Go back", GoBack) + Button("Go to Page A", GoToPageA) + Button("Go to Page B", GoToPageB) + }) + .gridRow(1) + } + ) + .title("Page C") diff --git a/samples/Navigation/NavigationPath/Platforms/Android/AndroidManifest.xml b/samples/Navigation/NavigationPath/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..bdec9b5 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Platforms/Android/MainActivity.fs b/samples/Navigation/NavigationPath/Platforms/Android/MainActivity.fs new file mode 100644 index 0000000..85f15ab --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Android/MainActivity.fs @@ -0,0 +1,17 @@ +namespace NavigationSample + +open Android.App +open Android.Content.PM +open Microsoft.Maui + +[] +type MainActivity() = + inherit MauiAppCompatActivity() diff --git a/samples/Navigation/NavigationPath/Platforms/Android/MainApplication.fs b/samples/Navigation/NavigationPath/Platforms/Android/MainApplication.fs new file mode 100644 index 0000000..7910a54 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Android/MainApplication.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Android.App +open Microsoft.Maui + +[] +type MainApplication(handle, ownership) = + inherit MauiApplication(handle, ownership) + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/NavigationPath/Platforms/Android/Resources/values/colors.xml b/samples/Navigation/NavigationPath/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..5cd1604 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Platforms/MacCatalyst/AppDelegate.fs b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/AppDelegate.fs new file mode 100644 index 0000000..90458e5 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override this.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Info.plist b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..0690e47 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Program.fs b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Program.fs new file mode 100644 index 0000000..26c87e7 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/MacCatalyst/Program.fs @@ -0,0 +1,9 @@ +namespace NavigationSample + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/NavigationPath/Platforms/Tizen/Main.fs b/samples/Navigation/NavigationPath/Platforms/Tizen/Main.fs new file mode 100644 index 0000000..3d59efd --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Tizen/Main.fs @@ -0,0 +1,16 @@ +namespace NavigationSample + +open System +open Microsoft.Maui +open Microsoft.Maui.Hosting + +type Program() = + inherit MauiApplication() + + override this.CreateMauiApp() = MauiProgram.CreateMauiApp() + +module Program = + [] + let main args = + let app = Program() + app.Run(args) diff --git a/samples/Navigation/NavigationPath/Platforms/Tizen/tizen-manifest.xml b/samples/Navigation/NavigationPath/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..83e2649 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + appicon.xhigh.png + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Platforms/Windows/App.fs b/samples/Navigation/NavigationPath/Platforms/Windows/App.fs new file mode 100644 index 0000000..ebceb21 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Windows/App.fs @@ -0,0 +1,10 @@ +namespace NavigationSample.WinUI + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +type App() = + inherit FSharp.Maui.WinUICompat.App() + + override this.CreateMauiApp() = + NavigationSample.MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/NavigationPath/Platforms/Windows/Main.fs b/samples/Navigation/NavigationPath/Platforms/Windows/Main.fs new file mode 100644 index 0000000..501c15d --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Windows/Main.fs @@ -0,0 +1,9 @@ +namespace NavigationSample.WinUI + +open System + +module Program = + [] + let main args = + do FSharp.Maui.WinUICompat.Program.Main(args, typeof) + 0 diff --git a/samples/Navigation/NavigationPath/Platforms/Windows/Package.appxmanifest b/samples/Navigation/NavigationPath/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..e98c6ed --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,43 @@ + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/NavigationPath/Platforms/Windows/app.manifest b/samples/Navigation/NavigationPath/Platforms/Windows/app.manifest new file mode 100644 index 0000000..c8a173c --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/samples/Navigation/NavigationPath/Platforms/iOS/AppDelegate.fs b/samples/Navigation/NavigationPath/Platforms/iOS/AppDelegate.fs new file mode 100644 index 0000000..7418834 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/iOS/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace NavigationSample + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/NavigationPath/Platforms/iOS/Info.plist b/samples/Navigation/NavigationPath/Platforms/iOS/Info.plist new file mode 100644 index 0000000..358337b --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Navigation/NavigationPath/Platforms/iOS/Program.fs b/samples/Navigation/NavigationPath/Platforms/iOS/Program.fs new file mode 100644 index 0000000..26c87e7 --- /dev/null +++ b/samples/Navigation/NavigationPath/Platforms/iOS/Program.fs @@ -0,0 +1,9 @@ +namespace NavigationSample + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/NavigationPath/Properties/launchSettings.json b/samples/Navigation/NavigationPath/Properties/launchSettings.json new file mode 100644 index 0000000..c16206a --- /dev/null +++ b/samples/Navigation/NavigationPath/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Resources/AppIcon/appicon.svg b/samples/Navigation/NavigationPath/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..5f04fcf --- /dev/null +++ b/samples/Navigation/NavigationPath/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Resources/AppIcon/appiconfg.svg b/samples/Navigation/NavigationPath/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/NavigationPath/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Regular.ttf b/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..534d009 Binary files /dev/null and b/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Semibold.ttf b/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..f333153 Binary files /dev/null and b/samples/Navigation/NavigationPath/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/samples/Navigation/NavigationPath/Resources/Images/dotnet_bot.svg b/samples/Navigation/NavigationPath/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..51b1c33 --- /dev/null +++ b/samples/Navigation/NavigationPath/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/NavigationPath/Resources/Raw/AboutAssets.txt b/samples/Navigation/NavigationPath/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..50b8a7b --- /dev/null +++ b/samples/Navigation/NavigationPath/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/samples/Navigation/NavigationPath/Resources/Splash/splash.svg b/samples/Navigation/NavigationPath/Resources/Splash/splash.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/NavigationPath/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/NavigationPath/Sample.fs b/samples/Navigation/NavigationPath/Sample.fs new file mode 100644 index 0000000..8cf08ba --- /dev/null +++ b/samples/Navigation/NavigationPath/Sample.fs @@ -0,0 +1,77 @@ +namespace NavigationSample + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// This is the root of the app +module Sample = + /// We instantiate a single NavigationController that will be used for the lifetime of the app + let nav = NavigationController() + + /// The Model needs only to store the current navigation stack + type Model = { Navigation: NavigationStack } + + type Msg = + | NavigationPushed of NavigationPath + | BackNavigated + | NavigationMsg of NavigationState.Msg + | BackButtonPressed + + /// This is where we subscribe to the navigation events + /// If a navigation forward is requested, we dispatch the NavigationPushed message + /// If a navigation backward is requested, we dispatch the BackNavigated message + let navSubscription () : Cmd = + [ fun dispatch -> + nav.Navigated.Add(fun path -> dispatch(NavigationPushed path)) + nav.BackNavigated.Add(fun () -> dispatch BackNavigated) ] + + /// In the init function, we initialize the NavigationStack and subscribe to the navigation events + let init () = + let rootPage = NavigationState.init NavigationPath.PageA + { Navigation = NavigationStack.Init(rootPage) }, navSubscription() + + let update msg model = + match msg with + | NavigationPushed path -> + // When a new path is pushed, we create a new page and push it on the stack + let newPage = NavigationState.init path + + { Navigation = model.Navigation.Push(newPage) }, Cmd.none + + | BackNavigated -> + // BackNavigated handles both the back button and the programmatic back navigation + // We simply pop the current page from the stack + { Navigation = model.Navigation.Pop() }, Cmd.none + + | NavigationMsg navMsg -> + let m, navCmd = NavigationState.update nav navMsg model.Navigation.CurrentPage + + { Navigation = model.Navigation.UpdateCurrentPage(m) }, Cmd.map NavigationMsg navCmd + + | BackButtonPressed -> + let m, navCmd = NavigationState.updateBackButton nav model.Navigation.CurrentPage + + { Navigation = model.Navigation.UpdateCurrentPage(m) }, Cmd.map NavigationMsg navCmd + + /// The view function contains the NavigationPage control that will display the different pages + /// and handle the navigation animations (push, pop) as well has displaying a back button by default + /// + /// Because of MVU, all the pages need to return the same Msg type but they all have their own. + /// To be able to wrap those Msgs into the app's root Msg type, we use the View.map helper function. + let view model = + Application( + (NavigationPage() { + // We inject in the NavigationPage history the back stack of our navigation + for navModel in List.rev model.Navigation.BackStack do + (View.map NavigationMsg (NavigationState.view navModel)).hasBackButton(false) + + // The page currently displayed is the one on top of the stack + (View.map NavigationMsg (NavigationState.view model.Navigation.CurrentPage)) + .hasBackButton(false) + }) + .onBackButtonPressed(BackButtonPressed) + ) + + let program = Program.statefulWithCmd init update view