diff --git a/Fabulous.MauiControls.sln b/Fabulous.MauiControls.sln index f747b8c..c2a39b8 100644 --- a/Fabulous.MauiControls.sln +++ b/Fabulous.MauiControls.sln @@ -53,6 +53,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BasicNavigation", "samples\ EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NavigationPath", "samples\Navigation\NavigationPath\NavigationPath.fsproj", "{5B3F6C4E-82CF-442F-BFB4-216C1CD85700}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ComponentNavigation", "samples\Navigation\ComponentNavigation\ComponentNavigation.fsproj", "{66532A61-1BB8-4BD1-A281-A160ABB0EFE7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +130,10 @@ Global {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 + {66532A61-1BB8-4BD1-A281-A160ABB0EFE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66532A61-1BB8-4BD1-A281-A160ABB0EFE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66532A61-1BB8-4BD1-A281-A160ABB0EFE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66532A61-1BB8-4BD1-A281-A160ABB0EFE7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {67FB01A1-1A3E-4A3B-83DC-7D63B56FB1A1} = {35A6823C-8312-4F92-818A-5117BB31A569} @@ -146,5 +152,6 @@ Global {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} + {66532A61-1BB8-4BD1-A281-A160ABB0EFE7} = {3A3581BD-4228-49B0-84D5-AF39D620BA34} EndGlobalSection EndGlobal diff --git a/samples/Navigation/ComponentNavigation/AppMsg.fs b/samples/Navigation/ComponentNavigation/AppMsg.fs new file mode 100644 index 0000000..83cfa0a --- /dev/null +++ b/samples/Navigation/ComponentNavigation/AppMsg.fs @@ -0,0 +1,17 @@ +namespace ComponentNavigation + +/// With each component runs in an isolated context, so we need a way to communicate between them. +/// We define application-wide messages here, and a dispatcher to send and receive messages. +[] +type AppMsg = | BackButtonPressed + +type IAppMessageDispatcher = + abstract Dispatched: IEvent + abstract member Dispatch: AppMsg -> unit + +type AppMessageDispatcher() = + let dispatched = Event() + + interface IAppMessageDispatcher with + member _.Dispatched = dispatched.Publish + member _.Dispatch(msg) = dispatched.Trigger(msg) diff --git a/samples/Navigation/ComponentNavigation/ComponentNavigation.fsproj b/samples/Navigation/ComponentNavigation/ComponentNavigation.fsproj new file mode 100644 index 0000000..7d26527 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/ComponentNavigation.fsproj @@ -0,0 +1,107 @@ + + + + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 + + + Exe + ComponentNavigation + true + true + false + + + ComponentNavigation + + + org.fabulous.maui.componentnavigation + 9abd223e-09e7-4649-b22b-7395cb4724e1 + + + 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/ComponentNavigation/MauiProgram.fs b/samples/Navigation/ComponentNavigation/MauiProgram.fs new file mode 100644 index 0000000..178671b --- /dev/null +++ b/samples/Navigation/ComponentNavigation/MauiProgram.fs @@ -0,0 +1,19 @@ +namespace ComponentNavigation + +open Microsoft.Maui.Hosting +open Fabulous.Maui + +type MauiProgram = + static member CreateMauiApp() = + let nav = NavigationController() + let appMsgDispatcher = AppMessageDispatcher() + + MauiApp + .CreateBuilder() + .UseFabulousApp(Sample.view nav appMsgDispatcher) + .ConfigureFonts(fun fonts -> + fonts + .AddFont("OpenSans-Regular.ttf", "OpenSansRegular") + .AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold") + |> ignore) + .Build() diff --git a/samples/Navigation/ComponentNavigation/Navigation.fs b/samples/Navigation/ComponentNavigation/Navigation.fs new file mode 100644 index 0000000..209f121 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Navigation.fs @@ -0,0 +1,66 @@ +namespace ComponentNavigation + +open Fabulous + +/// This is the centerpiece of navigating through paths: +/// A single enum regrouping all the navigation routes with their arguments +[] +type NavigationRoute = + | 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 navigationRequested = Event() + let backNavigationRequested = Event() + + member this.NavigationRequested = navigationRequested.Publish + member this.BackNavigationRequested = backNavigationRequested.Publish + + member this.NavigateTo(path: NavigationRoute) = navigationRequested.Trigger(path) + + member this.NavigateBack() = backNavigationRequested.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 NavigationRoute.PageA + + let navigateToPageB nav initialCount = + navigateTo nav (NavigationRoute.PageB initialCount) + + let navigateToPageC nav someArgs stepCount = + navigateTo nav (NavigationRoute.PageC(someArgs, stepCount)) + +/// 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: NavigationRoute list + CurrentPage: NavigationRoute + ForwardStack: NavigationRoute list } + + static member Init(path: NavigationRoute) = + { BackStack = [] + CurrentPage = path + ForwardStack = [] } + + member this.Push(path: NavigationRoute) = + { BackStack = this.CurrentPage :: this.BackStack + CurrentPage = path + ForwardStack = [] } + + member this.Pop() = + match this.BackStack with + | [] -> this + | head :: tail -> + { BackStack = tail + CurrentPage = head + ForwardStack = [] } + + member this.UpdateCurrentPage(path: NavigationRoute) = { this with CurrentPage = path } diff --git a/samples/Navigation/ComponentNavigation/PageA.fs b/samples/Navigation/ComponentNavigation/PageA.fs new file mode 100644 index 0000000..ecec7dc --- /dev/null +++ b/samples/Navigation/ComponentNavigation/PageA.fs @@ -0,0 +1,75 @@ +namespace ComponentNavigation + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// Each page is "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 = { IsActive: bool; Count: int } + + type Msg = + | Active + | Inactive + | Increment + | Decrement + | GoBack + | GoToPageB + | GoToPageC + | BackButtonPressed + + /// Since the NavigationRoute.PageA doesn't take arguments, the init function excepts a unit parameter. + let init () = + { IsActive = false; Count = 0 }, Cmd.none + + let update (nav: NavigationController) msg model = + match msg with + | Active -> { model with IsActive = true }, Cmd.none + | Inactive -> { model with IsActive = false }, Cmd.none + | Increment -> { model with Count = model.Count + 1 }, Cmd.none + | Decrement -> { model with 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 -> { model with Count = model.Count - 1 }, Cmd.none + + let subscribe (appMsgDispatcher: IAppMessageDispatcher) model = + let localAppMsgSub dispatch = + appMsgDispatcher.Dispatched.Subscribe(fun msg -> + match msg with + | AppMsg.BackButtonPressed -> dispatch BackButtonPressed) + + [ if model.IsActive then + [ nameof localAppMsgSub ], localAppMsgSub ] + + let program nav appMsgDispatcher = + Program.statefulWithCmd init (update nav) + |> Program.withSubscription(subscribe appMsgDispatcher) + + let view nav appMsgDispatcher = + Component(program nav appMsgDispatcher) { + let! model = Mvu.State + + 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") + .onNavigatedTo(Active) + .onNavigatedFrom(Inactive) + } diff --git a/samples/Navigation/ComponentNavigation/PageB.fs b/samples/Navigation/ComponentNavigation/PageB.fs new file mode 100644 index 0000000..569e8eb --- /dev/null +++ b/samples/Navigation/ComponentNavigation/PageB.fs @@ -0,0 +1,79 @@ +namespace ComponentNavigation + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageB = + type Model = + { IsActive: bool + InitialCount: int + Count: int } + + type Msg = + | Active + | Inactive + | Increment + | Decrement + | GoBack + | GoToPageA + | GoToPageC + + /// Contrary to PageA, NavigationPath.PageB has a initialCount argument so the init function will receive it. + let init initialCount = + { IsActive = false + InitialCount = initialCount + Count = initialCount }, + Cmd.none + + let update (nav: NavigationController) msg model = + match msg with + | Active -> { model with IsActive = true }, Cmd.none + | Inactive -> { model with IsActive = false }, Cmd.none + | 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 subscribe (appMsgDispatcher: IAppMessageDispatcher) model = + let localAppMsgSub dispatch = + appMsgDispatcher.Dispatched.Subscribe(fun msg -> + match msg with + | AppMsg.BackButtonPressed -> dispatch GoBack) + + [ if model.IsActive then + [ nameof localAppMsgSub ], localAppMsgSub ] + + let program nav appMsgDispatcher = + Program.statefulWithCmd init (update nav) + |> Program.withSubscription(subscribe appMsgDispatcher) + + let view nav appMsgDispatcher arg = + Component(program nav appMsgDispatcher, arg) { + let! model = Mvu.State + + 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") + .onNavigatedTo(Active) + .onNavigatedFrom(Inactive) + } diff --git a/samples/Navigation/ComponentNavigation/PageC.fs b/samples/Navigation/ComponentNavigation/PageC.fs new file mode 100644 index 0000000..94ffb1b --- /dev/null +++ b/samples/Navigation/ComponentNavigation/PageC.fs @@ -0,0 +1,87 @@ +namespace ComponentNavigation + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +module PageC = + type Model = + { IsActive: bool + Args: string + StepCount: int + Count: int } + + type Msg = + | Active + | Inactive + | Increment + | Decrement + | GoBack + | GoToPageA + | GoToPageB + + let init (args, stepCount) = + { IsActive = false + Args = args + StepCount = stepCount + Count = 0 }, + Cmd.none + + let update (nav: NavigationController) msg model = + match msg with + | Active -> { model with IsActive = true }, Cmd.none + | Inactive -> { model with IsActive = false }, Cmd.none + | 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 subscribe (appMsgDispatcher: IAppMessageDispatcher) model = + let localAppMsgSub dispatch = + appMsgDispatcher.Dispatched.Subscribe(fun msg -> + match msg with + | AppMsg.BackButtonPressed -> dispatch GoBack) + + [ if model.IsActive then + [ nameof localAppMsgSub ], localAppMsgSub ] + + let program nav appMsgDispatcher = + Program.statefulWithCmd init (update nav) + |> Program.withSubscription(subscribe appMsgDispatcher) + + let view nav appMsgDispatcher args = + Component(program nav appMsgDispatcher, args) { + let! model = Mvu.State + + 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") + .onNavigatedTo(Active) + .onNavigatedFrom(Inactive) + } diff --git a/samples/Navigation/ComponentNavigation/Platforms/Android/AndroidManifest.xml b/samples/Navigation/ComponentNavigation/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..bdec9b5 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/Navigation/ComponentNavigation/Platforms/Android/MainActivity.fs b/samples/Navigation/ComponentNavigation/Platforms/Android/MainActivity.fs new file mode 100644 index 0000000..36a0d24 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Android/MainActivity.fs @@ -0,0 +1,17 @@ +namespace ComponentNavigation + +open Android.App +open Android.Content.PM +open Microsoft.Maui + +[] +type MainActivity() = + inherit MauiAppCompatActivity() diff --git a/samples/Navigation/ComponentNavigation/Platforms/Android/MainApplication.fs b/samples/Navigation/ComponentNavigation/Platforms/Android/MainApplication.fs new file mode 100644 index 0000000..b1126da --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Android/MainApplication.fs @@ -0,0 +1,10 @@ +namespace ComponentNavigation + +open Android.App +open Microsoft.Maui + +[] +type MainApplication(handle, ownership) = + inherit MauiApplication(handle, ownership) + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/ComponentNavigation/Platforms/Android/Resources/values/colors.xml b/samples/Navigation/ComponentNavigation/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..5cd1604 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/AppDelegate.fs b/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/AppDelegate.fs new file mode 100644 index 0000000..da8c0cd --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace ComponentNavigation + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override this.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/Info.plist b/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..0690e47 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/MacCatalyst/Program.fs b/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/Program.fs new file mode 100644 index 0000000..5a46f87 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/MacCatalyst/Program.fs @@ -0,0 +1,9 @@ +namespace ComponentNavigation + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/ComponentNavigation/Platforms/Tizen/Main.fs b/samples/Navigation/ComponentNavigation/Platforms/Tizen/Main.fs new file mode 100644 index 0000000..3d59efd --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/Tizen/tizen-manifest.xml b/samples/Navigation/ComponentNavigation/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..83e2649 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/Windows/App.fs b/samples/Navigation/ComponentNavigation/Platforms/Windows/App.fs new file mode 100644 index 0000000..ebceb21 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/Windows/Main.fs b/samples/Navigation/ComponentNavigation/Platforms/Windows/Main.fs new file mode 100644 index 0000000..501c15d --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/Windows/Package.appxmanifest b/samples/Navigation/ComponentNavigation/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..e98c6ed --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,43 @@ + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/ComponentNavigation/Platforms/Windows/app.manifest b/samples/Navigation/ComponentNavigation/Platforms/Windows/app.manifest new file mode 100644 index 0000000..c8a173c --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/samples/Navigation/ComponentNavigation/Platforms/iOS/AppDelegate.fs b/samples/Navigation/ComponentNavigation/Platforms/iOS/AppDelegate.fs new file mode 100644 index 0000000..e4d3f47 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/iOS/AppDelegate.fs @@ -0,0 +1,10 @@ +namespace ComponentNavigation + +open Foundation +open Microsoft.Maui + +[] +type AppDelegate() = + inherit MauiUIApplicationDelegate() + + override _.CreateMauiApp() = MauiProgram.CreateMauiApp() diff --git a/samples/Navigation/ComponentNavigation/Platforms/iOS/Info.plist b/samples/Navigation/ComponentNavigation/Platforms/iOS/Info.plist new file mode 100644 index 0000000..358337b --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Platforms/iOS/Program.fs b/samples/Navigation/ComponentNavigation/Platforms/iOS/Program.fs new file mode 100644 index 0000000..5a46f87 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Platforms/iOS/Program.fs @@ -0,0 +1,9 @@ +namespace ComponentNavigation + +open UIKit + +module Program = + [] + let main args = + UIApplication.Main(args, null, typeof) + 0 diff --git a/samples/Navigation/ComponentNavigation/Properties/launchSettings.json b/samples/Navigation/ComponentNavigation/Properties/launchSettings.json new file mode 100644 index 0000000..c16206a --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Resources/AppIcon/appicon.svg b/samples/Navigation/ComponentNavigation/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..5f04fcf --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/Navigation/ComponentNavigation/Resources/AppIcon/appiconfg.svg b/samples/Navigation/ComponentNavigation/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Regular.ttf b/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..534d009 Binary files /dev/null and b/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Semibold.ttf b/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..f333153 Binary files /dev/null and b/samples/Navigation/ComponentNavigation/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/samples/Navigation/ComponentNavigation/Resources/Images/dotnet_bot.svg b/samples/Navigation/ComponentNavigation/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..51b1c33 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Navigation/ComponentNavigation/Resources/Raw/AboutAssets.txt b/samples/Navigation/ComponentNavigation/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..50b8a7b --- /dev/null +++ b/samples/Navigation/ComponentNavigation/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/ComponentNavigation/Resources/Splash/splash.svg b/samples/Navigation/ComponentNavigation/Resources/Splash/splash.svg new file mode 100644 index 0000000..62d66d7 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/Navigation/ComponentNavigation/Sample.fs b/samples/Navigation/ComponentNavigation/Sample.fs new file mode 100644 index 0000000..a006bf0 --- /dev/null +++ b/samples/Navigation/ComponentNavigation/Sample.fs @@ -0,0 +1,67 @@ +namespace ComponentNavigation + +open Fabulous +open Fabulous.Maui + +open type Fabulous.Maui.View + +/// This is the root of the app +module Sample = + /// The Model needs only to store the current navigation stack + type Model = { Navigation: NavigationStack } + + type Msg = + | NavigationMsg of NavigationRoute + | BackNavigationMsg + | BackButtonPressed + + let notifyBackButtonPressed (appMessageDispatcher: IAppMessageDispatcher) = + Cmd.ofEffect(fun _ -> appMessageDispatcher.Dispatch(AppMsg.BackButtonPressed)) + + /// In the init function, we initialize the NavigationStack + let init () = + { Navigation = NavigationStack.Init(NavigationRoute.PageA) }, Cmd.none + + let update appMsgDispatcher msg model = + match msg with + | NavigationMsg route -> { Navigation = model.Navigation.Push(route) }, Cmd.none + | BackNavigationMsg -> { Navigation = model.Navigation.Pop() }, Cmd.none + | BackButtonPressed -> model, notifyBackButtonPressed appMsgDispatcher + + let subscribe (nav: NavigationController) _ = + let navRequestedSub dispatch = + nav.NavigationRequested.Subscribe(fun route -> dispatch(NavigationMsg route)) + + let backNavRequestedSub dispatch = + nav.BackNavigationRequested.Subscribe(fun () -> dispatch BackNavigationMsg) + + [ [ nameof navRequestedSub ], navRequestedSub + [ nameof backNavRequestedSub ], backNavRequestedSub ] + + let program nav appMsgDispatcher = + Program.statefulWithCmd init (update appMsgDispatcher) + |> Program.withSubscription(subscribe nav) + + let navView nav appMsgDispatcher (path: NavigationRoute) = + match path with + | NavigationRoute.PageA -> AnyPage(PageA.view nav appMsgDispatcher) + | NavigationRoute.PageB initialCount -> AnyPage(PageB.view nav appMsgDispatcher initialCount) + | NavigationRoute.PageC(someArgs, stepCount) -> AnyPage(PageC.view nav appMsgDispatcher (someArgs, stepCount)) + + let view nav appMsgDispatcher () = + Component(program nav appMsgDispatcher) { + let! model = Mvu.State + + Application( + (NavigationPage() { + // We inject in the NavigationPage history the back stack of our navigation + for navPath in List.rev model.Navigation.BackStack do + navView nav appMsgDispatcher navPath + + // The page currently displayed is the one on top of the stack + navView nav appMsgDispatcher model.Navigation.CurrentPage + }) + .onBackButtonPressed(BackButtonPressed) + .onBackNavigated(BackNavigationMsg) + ) + } diff --git a/src/Fabulous.MauiControls/Views/Application.fs b/src/Fabulous.MauiControls/Views/Application.fs index 5b39a39..a5551b3 100644 --- a/src/Fabulous.MauiControls/Views/Application.fs +++ b/src/Fabulous.MauiControls/Views/Application.fs @@ -46,7 +46,11 @@ type FabApplication() = override this.OnAppLinkRequestReceived(uri) = appLinkRequestReceived.Trigger(this, uri) - override this.CreateWindow(activationState) = windows[0] + override this.CreateWindow(activationState) = + if windows.Count > 0 then + windows[0] + else + base.CreateWindow(activationState) override this.OpenWindow(window) = windows.Add(window) diff --git a/src/Fabulous.MauiControls/Views/Pages/_Page.fs b/src/Fabulous.MauiControls/Views/Pages/_Page.fs index e2a2e79..22a1ea5 100644 --- a/src/Fabulous.MauiControls/Views/Pages/_Page.fs +++ b/src/Fabulous.MauiControls/Views/Pages/_Page.fs @@ -27,6 +27,12 @@ module Page = let IsBusy = Attributes.defineBindableBool Page.IsBusyProperty + let NavigatedTo = + Attributes.defineEvent "NavigatedTo" (fun target -> (target :?> Page).NavigatedTo) + + let NavigatedFrom = + Attributes.defineEvent "NavigatedFrom" (fun target -> (target :?> Page).NavigatedFrom) + let Padding = Attributes.defineBindableWithEquality Page.PaddingProperty let Title = Attributes.defineBindableWithEquality Page.TitleProperty @@ -82,6 +88,14 @@ type PageModifiers = static member inline onDisappearing(this: WidgetBuilder<'msg, #IFabPage>, msg: 'msg) = this.AddScalar(Page.Disappearing.WithValue(MsgValue(msg))) + [] + static member inline onNavigatedTo(this: WidgetBuilder<'msg, #IFabPage>, msg: 'msg) = + this.AddScalar(Page.NavigatedTo.WithValue(fun _ -> msg)) + + [] + static member inline onNavigatedFrom(this: WidgetBuilder<'msg, #IFabPage>, msg: 'msg) = + this.AddScalar(Page.NavigatedFrom.WithValue(fun _ -> msg)) + /// Set the padding inside the widget /// Current widget /// The padding value