diff --git a/CHANGELOG.md b/CHANGELOG.md index 7094cec..fa582d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 _No unreleased changes_ +## [8.0.2] - 2023-12-12 + +### Added +- Added additional Any widgets to support Memo widget as a root by @TimLariviere (https://github.com/fabulous-dev/Fabulous.MauiControls/pull/52) +- Added missing Cell.ContextActions and MultiPage.CurrentPage modifiers by @TimLariviere (https://github.com/fabulous-dev/Fabulous.MauiControls/pull/52) + ## [8.0.1] - 2023-11-14 ### Fixed @@ -134,7 +140,8 @@ Essentially v2.8.1 and v8.0.0 are similar except for the required .NET version. ### Changed - Fabulous.MauiControls has moved from the Fabulous repository to its own repository: [https://github.com/fabulous-dev/Fabulous.MauiControls](https://github.com/fabulous-dev/Fabulous.MauiControls) -[unreleased]: https://github.com/fabulous-dev/Fabulous.MauiControls/compare/8.0.1...HEAD +[unreleased]: https://github.com/fabulous-dev/Fabulous.MauiControls/compare/8.0.2...HEAD +[8.0.2]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.2 [8.0.1]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.1 [8.0.0]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.0 [2.8.1]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/2.8.1 diff --git a/src/Fabulous.MauiControls/AppHostBuilderExtensions.fs b/src/Fabulous.MauiControls/AppHostBuilderExtensions.fs index c50b8b6..22c5b7a 100644 --- a/src/Fabulous.MauiControls/AppHostBuilderExtensions.fs +++ b/src/Fabulous.MauiControls/AppHostBuilderExtensions.fs @@ -12,6 +12,14 @@ type AppHostBuilderExtensions = static member UseFabulousApp(this: MauiAppBuilder, program: Program) : MauiAppBuilder = this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplication program) :> Microsoft.Maui.IApplication) + [] + static member UseFabulousApp(this: MauiAppBuilder, program: Program>) : MauiAppBuilder = + this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationMemo program) :> Microsoft.Maui.IApplication) + [] static member UseFabulousApp(this: MauiAppBuilder, program: Program<'arg, 'model, 'msg, #IFabApplication>, arg: 'arg) : MauiAppBuilder = this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationWithArgs arg program) :> Microsoft.Maui.IApplication) + + [] + static member UseFabulousApp(this: MauiAppBuilder, program: Program<'arg, 'model, 'msg, Memo.Memoized<#IFabApplication>>, arg: 'arg) : MauiAppBuilder = + this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationWithArgsMemo arg program) :> Microsoft.Maui.IApplication) diff --git a/src/Fabulous.MauiControls/Program.fs b/src/Fabulous.MauiControls/Program.fs index 473edc2..9cbe6a5 100644 --- a/src/Fabulous.MauiControls/Program.fs +++ b/src/Fabulous.MauiControls/Program.fs @@ -120,6 +120,14 @@ module Program = = define init update view + /// Create a program using an MVU loop. Add support for Cmd + let statefulWithCmdMemo + (init: 'arg -> 'model * Cmd<'msg>) + (update: 'msg -> 'model -> 'model * Cmd<'msg>) + (view: 'model -> WidgetBuilder<'msg, Memo.Memoized<#IFabApplication>>) + = + define init update view + /// Create a program using an MVU loop. Add support for CmdMsg let statefulWithCmdMsg (init: 'arg -> 'model * 'cmdMsg list) @@ -141,6 +149,17 @@ module Program = /// Start the program let startApplication (program: Program) : Application = startApplicationWithArgs () program + /// Start the program + let startApplicationWithArgsMemo (arg: 'arg) (program: Program<'arg, 'model, 'msg, Memo.Memoized<#IFabApplication>>) : Application = + let runner = Runners.create program + runner.Start(arg) + let adapter = ViewAdapters.create ViewNode.get runner + let view = adapter.CreateView() + unbox view + + /// Start the program + let startApplicationMemo (program: Program>) : Application = startApplicationWithArgsMemo () program + /// Subscribe to external source of events. /// The subscription is called once - with the initial model, but can dispatch new messages at any time. let withSubscription (subscribe: 'model -> Cmd<'msg>) (program: Program<'arg, 'model, 'msg, 'marker>) = diff --git a/src/Fabulous.MauiControls/Views/Any.fs b/src/Fabulous.MauiControls/Views/Any.fs index 1fdb8e8..0b7debd 100644 --- a/src/Fabulous.MauiControls/Views/Any.fs +++ b/src/Fabulous.MauiControls/Views/Any.fs @@ -12,12 +12,27 @@ module AnyBuilders = static member AnyView(this: WidgetBuilder<'msg, #IFabView>) = WidgetBuilder<'msg, IFabView>(this.Key, this.Attributes) + /// Downcast to View widget to allow to return different types of views in a single expression (e.g. if/else, match with pattern, etc.) + /// Current widget + static member AnyView(this: WidgetBuilder<'msg, Memo.Memoized<#IFabView>>) = + WidgetBuilder<'msg, Memo.Memoized>(this.Key, this.Attributes) + /// Downcast to Page widget to allow to return different types of pages in a single expression (e.g. if/else, match with pattern, etc.) /// Current widget static member AnyPage(this: WidgetBuilder<'msg, #IFabPage>) = WidgetBuilder<'msg, IFabPage>(this.Key, this.Attributes) + /// Downcast to Page widget to allow to return different types of pages in a single expression (e.g. if/else, match with pattern, etc.) + /// Current widget + static member AnyPage(this: WidgetBuilder<'msg, Memo.Memoized<#IFabPage>>) = + WidgetBuilder<'msg, Memo.Memoized>(this.Key, this.Attributes) + /// Downcast to Cell widget to allow to return different types of cells in a single expression (e.g. if/else, match with pattern, etc.) /// Current widget static member AnyCell(this: WidgetBuilder<'msg, #IFabCell>) = WidgetBuilder<'msg, IFabCell>(this.Key, this.Attributes) + + /// Downcast to Cell widget to allow to return different types of cells in a single expression (e.g. if/else, match with pattern, etc.) + /// Current widget + static member AnyCell(this: WidgetBuilder<'msg, Memo.Memoized<#IFabCell>>) = + WidgetBuilder<'msg, Memo.Memoized>(this.Key, this.Attributes) diff --git a/src/Fabulous.MauiControls/Views/Cells/_Cell.fs b/src/Fabulous.MauiControls/Views/Cells/_Cell.fs index a7e8476..f7ebfdf 100644 --- a/src/Fabulous.MauiControls/Views/Cells/_Cell.fs +++ b/src/Fabulous.MauiControls/Views/Cells/_Cell.fs @@ -2,6 +2,7 @@ namespace Fabulous.Maui open System.Runtime.CompilerServices open Fabulous +open Fabulous.StackAllocatedCollections open Microsoft.Maui.Controls type IFabCell = @@ -30,6 +31,9 @@ module Cell = let Tapped = Attributes.defineEventNoArg "Cell_Tapped" (fun target -> (target :?> Cell).Tapped) + let ContextActions = + Attributes.defineListWidgetCollection "Cell_ContextActions" (fun target -> (target :?> Cell).ContextActions) + [] type CellModifiers = /// Set a value that indicates whether the cell is enabled @@ -66,3 +70,19 @@ type CellModifiers = [] static member inline onTapped(this: WidgetBuilder<'msg, #IFabCell>, msg: 'msg) = this.AddScalar(Cell.Tapped.WithValue(MsgValue(msg))) + + /// Set the context actions of the cell + /// Current widget + [] + static member inline contextActions<'msg, 'marker when 'marker :> IFabCell>(this: WidgetBuilder<'msg, 'marker>) = + WidgetHelpers.buildAttributeCollection<'msg, 'marker, IFabMenuItem> Cell.ContextActions this + +[] +type CellYieldExtensions = + [] + static member inline Yield<'msg, 'marker, 'itemType when 'marker :> IFabCell and 'itemType :> IFabMenuItem> + ( + _: AttributeCollectionBuilder<'msg, 'marker, IFabMenuItem>, + x: WidgetBuilder<'msg, 'itemType> + ) : Content<'msg> = + { Widgets = MutStackArray1.One(x.Compile()) } diff --git a/src/Fabulous.MauiControls/Views/Pages/_MultiPageOfPage.fs b/src/Fabulous.MauiControls/Views/Pages/_MultiPageOfPage.fs index 0a9312e..54772ab 100644 --- a/src/Fabulous.MauiControls/Views/Pages/_MultiPageOfPage.fs +++ b/src/Fabulous.MauiControls/Views/Pages/_MultiPageOfPage.fs @@ -1,6 +1,8 @@ namespace Fabulous.Maui +open System open Fabulous +open Fabulous.ScalarAttributeDefinitions open Microsoft.Maui.Controls open System.Runtime.CompilerServices @@ -11,14 +13,69 @@ module MultiPageOfPage = let Children = Attributes.defineListWidgetCollection "MultiPageOfPage" (fun target -> (target :?> MultiPage).Children) + [] let CurrentPageChanged = Attributes.defineEventNoArg "MultiPageOfPage_CurrentPageChanged" (fun target -> (target :?> MultiPage).CurrentPageChanged) + let CurrentPageWithEvent = + let name = "MultiPageOfPage_CurrentPageWithEvent" + + let key = + SimpleScalarAttributeDefinition.CreateAttributeData( + ScalarAttributeComparers.noCompare, + (fun oldValueOpt (newValueOpt: ValueEventData voption) node -> + let target = node.Target :?> MultiPage + let event = target.CurrentPageChanged + + match newValueOpt with + | ValueNone -> + // The attribute is no longer applied, so we clean up the event + match node.TryGetHandler(name) with + | ValueNone -> () + | ValueSome handler -> event.RemoveHandler(handler) + + // Only clear the property if a value was set before + match oldValueOpt with + | ValueNone -> () + | ValueSome _ -> target.CurrentPage <- target.Children.[0] + + | ValueSome curr -> + // Clean up the old event handler if any + match node.TryGetHandler(name) with + | ValueNone -> () + | ValueSome handler -> event.RemoveHandler(handler) + + // Set the new value + target.CurrentPage <- target.Children.[curr.Value] + + // Set the new event handler + let handler = + EventHandler(fun sender args -> + let multiPage = sender :?> MultiPage + let currentPageIndex = multiPage.Children.IndexOf(multiPage.CurrentPage) + let (MsgValue r) = curr.Event currentPageIndex + Dispatcher.dispatch node r) + + node.SetHandler(name, ValueSome handler) + event.AddHandler(handler)) + ) + |> AttributeDefinitionStore.registerScalar + + { Key = key; Name = name }: SimpleScalarAttributeDefinition> + [] type MultiPageOfPageModifiers = /// Listen for the CurrentPageChanged event /// Current widget /// Message to dispatch - [] + [] static member inline onCurrentPageChanged(this: WidgetBuilder<'msg, #IFabMultiPageOfPage>, msg: 'msg) = this.AddScalar(MultiPageOfPage.CurrentPageChanged.WithValue(MsgValue(msg))) + + /// Set the current page and listen for changes + /// Current widget + /// The current page index + /// Function to invoke + [] + static member inline currentPage(this: WidgetBuilder<'msg, #IFabMultiPageOfPage>, currentPage: int, onCurrentPageChanged: int -> 'msg) = + this.AddScalar(MultiPageOfPage.CurrentPageWithEvent.WithValue(ValueEventData.create currentPage onCurrentPageChanged))