Skip to content

Commit

Permalink
Add Memo support for Any* + itemsLayout for CollectionView
Browse files Browse the repository at this point in the history
  • Loading branch information
TimLariviere committed Nov 26, 2023
1 parent 90fe2e7 commit 3cd1735
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 78 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

No unreleased changes.

## [2.4.1] - 2023-11-26

### Added
- Added additional Any widgets to support Memo widget as a root
- Added itemsLayout modifier to CollectionView widget

## [2.4.0] - 2023-11-22

### Changed
Expand Down Expand Up @@ -50,7 +56,8 @@ No unreleased changes.
### Changed
- Fabulous.XamarinForms has moved from the Fabulous repository to its own repository: [https://github.com/fabulous-dev/Fabulous.XamarinForms](https://github.com/fabulous-dev/Fabulous.XamarinForms)

[unreleased]: https://github.com/fabulous-dev/Fabulous.XamarinForms/compare/2.4.0...HEAD
[unreleased]: https://github.com/fabulous-dev/Fabulous.XamarinForms/compare/2.4.1...HEAD
[2.4.1]: https://github.com/fabulous-dev/Fabulous.XamarinForms/releases/tag/2.4.1
[2.4.0]: https://github.com/fabulous-dev/Fabulous.XamarinForms/releases/tag/2.4.0
[2.3.0]: https://github.com/fabulous-dev/Fabulous.XamarinForms/releases/tag/2.3.0
[2.2.0]: https://github.com/fabulous-dev/Fabulous.XamarinForms/releases/tag/2.2.0
Expand Down
9 changes: 8 additions & 1 deletion src/Fabulous.XamarinForms/Fabulous.XamarinForms.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<PackageId>Fabulous.XamarinForms</PackageId>
Expand Down Expand Up @@ -95,6 +95,13 @@
<Compile Include="Views\Collections\_ItemsView.fs" />
<Compile Include="Views\Collections\_ItemsViewOfCell.fs" />
<Compile Include="Views\Collections\ListView.fs" />
<Compile Include="Views\Collections\_ItemsLayout.fs" />
<Compile Include="Views\Collections\GridItemsLayout.fs" />
<Compile Include="Views\Collections\LinearItemsLayout.fs" />
<Compile Include="Views\Collections\_StructuredItemsView.fs" />
<Compile Include="Views\Collections\_SelectableItemsView.fs" />
<Compile Include="Views\Collections\_GroupableItemsView.fs" />
<Compile Include="Views\Collections\_ReorderableItemsView.fs" />
<Compile Include="Views\Collections\CollectionView.fs" />
<Compile Include="Views\Collections\CarouselView.fs" />
<Compile Include="Views\Shapes\Segments\_PathSegment.fs" />
Expand Down
32 changes: 32 additions & 0 deletions src/Fabulous.XamarinForms/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

open System
open Fabulous
open Fabulous.Memo
open Fabulous.ScalarAttributeDefinitions
open Fabulous.WidgetCollectionAttributeDefinitions
open Xamarin.Forms
open System.Diagnostics

module ViewHelpers =
let private tryGetSmallScalarValue (widget: Widget) (def: SmallScalarAttributeDefinition<'data>) =
match widget.ScalarAttributes with
| ValueNone -> ValueNone
| ValueSome scalarAttrs ->
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = def.Key) scalarAttrs with
| None -> ValueNone
| Some attr -> ValueSome(unbox<'data> attr.Value)

let private tryGetScalarValue (widget: Widget) (def: SimpleScalarAttributeDefinition<'data>) =
match widget.ScalarAttributes with
| ValueNone -> ValueNone
Expand All @@ -34,6 +43,8 @@ module ViewHelpers =
if def.TargetType <> null then
if def.TargetType.IsAssignableFrom(typeof<NavigationPage>) then
canReuseNavigationPage prev curr
elif def.TargetType.IsAssignableFrom(typeof<ItemsLayout>) then
canReuseItemsLayout prev curr
else
true
else
Expand Down Expand Up @@ -72,6 +83,13 @@ module ViewHelpers =
true

| _ -> true

/// Given the Orientation of the ItemsLayout is passed to the constructor, it can't be changed later.
/// If the orientation changes, a new ItemsLayout has to be created.
and private canReuseItemsLayout (prev: Widget) (curr: Widget) =
let prevOpt = tryGetSmallScalarValue prev ItemsLayout.Orientation
let currOpt = tryGetSmallScalarValue curr ItemsLayout.Orientation
prevOpt = currOpt

let defaultLogger () =
let log (level, message) =
Expand Down Expand Up @@ -115,6 +133,10 @@ module Program =
let statefulWithCmd (init: 'arg -> 'model * Cmd<'msg>) (update: 'msg -> 'model -> 'model * Cmd<'msg>) (view: 'model -> WidgetBuilder<'msg, #IApplication>) =
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, Memoized<#IApplication>>) =
define init update view

/// Create a program using an MVU loop. Add support for CmdMsg
let statefulWithCmdMsg
(init: 'arg -> 'model * 'cmdMsg list)
Expand All @@ -136,6 +158,16 @@ module Program =
/// Start the program
let startApplication (program: Program<unit, 'model, 'msg, 'marker>) : Application = startApplicationWithArgs () program

/// Start the program
let startApplicationWithArgsMemo (arg: 'arg) (program: Program<'arg, 'model, 'msg, Memoized<#IApplication>>) : Application =
let runner = Runners.create program
runner.Start(arg)
let adapter = ViewAdapters.create ViewNode.get runner
adapter.CreateView() |> unbox

/// Start the program
let startApplicationMemo (program: Program<unit, 'model, 'msg, Memoized<'marker>>) : 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>) =
Expand Down
13 changes: 13 additions & 0 deletions src/Fabulous.XamarinForms/Views/Any.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Fabulous.XamarinForms

open Fabulous
open Fabulous.Memo
open Fabulous.XamarinForms

[<AutoOpen>]
Expand All @@ -11,10 +12,22 @@ module AnyBuilders =
static member AnyView<'msg, 'marker when 'marker :> IView>(widget: WidgetBuilder<'msg, 'marker>) =
WidgetBuilder<'msg, IView>(widget.Key, widget.Attributes)

/// Downcast to IView to allow to return different types of views in a single expression (e.g. if/else, match with pattern, etc.)
static member AnyView<'msg, 'marker when 'marker :> IView>(widget: WidgetBuilder<'msg, Memoized<'marker>>) =
WidgetBuilder<'msg, Memoized<IView>>(widget.Key, widget.Attributes)

/// Downcast to IPage to allow to return different types of pages in a single expression (e.g. if/else, match with pattern, etc.)
static member AnyPage<'msg, 'marker when 'marker :> IPage>(widget: WidgetBuilder<'msg, 'marker>) =
WidgetBuilder<'msg, IPage>(widget.Key, widget.Attributes)

/// Downcast to IPage to allow to return different types of pages in a single expression (e.g. if/else, match with pattern, etc.)
static member AnyPage<'msg, 'marker when 'marker :> IPage>(widget: WidgetBuilder<'msg, Memoized<'marker>>) =
WidgetBuilder<'msg, Memoized<IPage>>(widget.Key, widget.Attributes)

/// Downcast to ICell to allow to return different types of cells in a single expression (e.g. if/else, match with pattern, etc.)
static member AnyCell<'msg, 'marker when 'marker :> ICell>(widget: WidgetBuilder<'msg, 'marker>) =
WidgetBuilder<'msg, ICell>(widget.Key, widget.Attributes)

/// Downcast to ICell to allow to return different types of cells in a single expression (e.g. if/else, match with pattern, etc.)
static member AnyCell<'msg, 'marker when 'marker :> ICell>(widget: WidgetBuilder<'msg, Memoized<'marker>>) =
WidgetBuilder<'msg, Memoized<ICell>>(widget.Key, widget.Attributes)
75 changes: 2 additions & 73 deletions src/Fabulous.XamarinForms/Views/Collections/CollectionView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,11 @@ open Fabulous
open Xamarin.Forms

type ICollectionView =
inherit IItemsView
inherit IReordableItemsView

module CollectionView =
let WidgetKey = Widgets.register<CollectionView>()

let GroupedItemsSource =
Attributes.defineSimpleScalar<GroupedWidgetItems>
"CollectionView_GroupedItemsSource"
(fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems)
(fun _ newValueOpt node ->
let collectionView = node.Target :?> CollectionView

match newValueOpt with
| ValueNone ->
collectionView.IsGrouped <- false
collectionView.ClearValue(CollectionView.ItemsSourceProperty)
collectionView.ClearValue(CollectionView.GroupHeaderTemplateProperty)
collectionView.ClearValue(CollectionView.GroupFooterTemplateProperty)
collectionView.ClearValue(CollectionView.ItemTemplateProperty)

| ValueSome value ->
collectionView.IsGrouped <- true

collectionView.SetValue(CollectionView.ItemTemplateProperty, WidgetDataTemplateSelector(node, unbox >> value.ItemTemplate))

collectionView.SetValue(CollectionView.GroupHeaderTemplateProperty, WidgetDataTemplateSelector(node, unbox >> value.HeaderTemplate))

if value.FooterTemplate.IsSome then
collectionView.SetValue(
CollectionView.GroupFooterTemplateProperty,
WidgetDataTemplateSelector(node, unbox >> value.FooterTemplate.Value)
)

collectionView.SetValue(CollectionView.ItemsSourceProperty, value.OriginalItems))

let SelectionMode =
Attributes.defineBindableEnum<SelectionMode> CollectionView.SelectionModeProperty

let Header = Attributes.defineBindableWidget CollectionView.HeaderProperty

let Footer = Attributes.defineBindableWidget CollectionView.FooterProperty

let ItemSizingStrategy =
Attributes.defineBindableEnum<ItemSizingStrategy> CollectionView.ItemSizingStrategyProperty

let SelectionChanged =
Attributes.defineEvent<SelectionChangedEventArgs> "CollectionView_SelectionChanged" (fun target -> (target :?> CollectionView).SelectionChanged)

[<AutoOpen>]
module CollectionViewBuilders =
type Fabulous.XamarinForms.View with
Expand All @@ -66,40 +23,12 @@ module CollectionViewBuilders =
=
WidgetHelpers.buildGroupItems<'msg, ICollectionView, 'groupData, 'itemData, 'groupMarker, 'itemMarker>
CollectionView.WidgetKey
CollectionView.GroupedItemsSource
GroupableItemsView.GroupedItemsSource
items

[<Extension>]
type CollectionViewModifiers =

[<Extension>]
static member inline selectionMode(this: WidgetBuilder<'msg, #ICollectionView>, value: SelectionMode) =
this.AddScalar(CollectionView.SelectionMode.WithValue(value))

[<Extension>]
static member inline onSelectionChanged(this: WidgetBuilder<'msg, #ICollectionView>, onSelectionChanged: SelectionChangedEventArgs -> 'msg) =
this.AddScalar(CollectionView.SelectionChanged.WithValue(fun args -> onSelectionChanged args |> box))

[<Extension>]
static member inline header<'msg, 'marker, 'contentMarker when 'marker :> ICollectionView and 'contentMarker :> IView>
(
this: WidgetBuilder<'msg, 'marker>,
content: WidgetBuilder<'msg, 'contentMarker>
) =
this.AddWidget(CollectionView.Header.WithValue(content.Compile()))

[<Extension>]
static member inline footer<'msg, 'marker, 'contentMarker when 'marker :> ICollectionView and 'contentMarker :> IView>
(
this: WidgetBuilder<'msg, 'marker>,
content: WidgetBuilder<'msg, 'contentMarker>
) =
this.AddWidget(CollectionView.Footer.WithValue(content.Compile()))

[<Extension>]
static member inline itemSizingStrategy(this: WidgetBuilder<'msg, #ICollectionView>, value: ItemSizingStrategy) =
this.AddScalar(CollectionView.ItemSizingStrategy.WithValue(value))

/// <summary>Link a ViewRef to access the direct CollectionView control instance</summary>
[<Extension>]
static member inline reference(this: WidgetBuilder<'msg, ICollectionView>, value: ViewRef<CollectionView>) =
Expand Down
64 changes: 64 additions & 0 deletions src/Fabulous.XamarinForms/Views/Collections/GridItemsLayout.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Fabulous.XamarinForms

open System.Runtime.CompilerServices
open Fabulous
open Fabulous.ScalarAttributeDefinitions
open Xamarin.Forms

type IGridItemsLayout = inherit Fabulous.XamarinForms.IItemsLayout

module GridItemsLayout =
let Span = Attributes.defineBindableInt GridItemsLayout.SpanProperty

let WidgetKey = Widgets.registerWithFactory(fun widget ->
let span =
match widget.ScalarAttributes with
| ValueNone -> ValueNone
| ValueSome attrs ->
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Span.Key) attrs with
| None -> ValueNone
| Some attr -> ValueSome (SmallScalars.Int.decode attr.NumericValue)

let orientation =
match widget.ScalarAttributes with
| ValueNone -> failwith "GridItemsLayout must have an orientation attribute"
| ValueSome attrs ->
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = ItemsLayout.Orientation.Key) attrs with
| None -> failwith "GridItemsLayout must have an orientation attribute"
| Some attr -> SmallScalars.IntEnum.decode<ItemsLayoutOrientation> attr.NumericValue

match span with
| ValueNone -> GridItemsLayout(orientation)
| ValueSome span -> GridItemsLayout(span, orientation)
)

let HorizontalItemSpacing = Attributes.defineBindableFloat GridItemsLayout.HorizontalItemSpacingProperty
let VerticalItemSpacing = Attributes.defineBindableFloat GridItemsLayout.VerticalItemSpacingProperty

[<AutoOpen>]
module GridItemsBuilders =
type Fabulous.XamarinForms.View with
static member inline GridItemsLayout(orientation: ItemsLayoutOrientation) =
WidgetBuilder<'msg, IGridItemsLayout>(
GridItemsLayout.WidgetKey,
ItemsLayout.Orientation.WithValue(orientation)
)

[<Extension>]
type GridItemsLayoutExtensions =
[<Extension>]
static member inline horizontalItemSpacing(this: WidgetBuilder<'msg, #IGridItemsLayout>, value: float) =
this.AddScalar(GridItemsLayout.HorizontalItemSpacing.WithValue(value))

[<Extension>]
static member inline span(this: WidgetBuilder<'msg, #IGridItemsLayout>, value: int) =
this.AddScalar(GridItemsLayout.Span.WithValue(value))

[<Extension>]
static member inline verticalItemSpacing(this: WidgetBuilder<'msg, #IGridItemsLayout>, value: float) =
this.AddScalar(GridItemsLayout.VerticalItemSpacing.WithValue(value))

/// <summary>Link a ViewRef to access the direct GridItemsLayout control instance</summary>
[<Extension>]
static member inline reference(this: WidgetBuilder<'msg, IListView>, value: ViewRef<GridItemsLayout>) =
this.AddScalar(ViewRefAttributes.ViewRef.WithValue(value.Unbox))
42 changes: 42 additions & 0 deletions src/Fabulous.XamarinForms/Views/Collections/LinearItemsLayout.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Fabulous.XamarinForms

open System.Runtime.CompilerServices
open Fabulous
open Xamarin.Forms

type ILinearItemsLayout = inherit Fabulous.XamarinForms.IItemsLayout

module LinearItemsLayout =
let WidgetKey = Widgets.registerWithFactory(fun widget ->
let orientation =
match widget.ScalarAttributes with
| ValueNone -> failwith "LinearItemsLayout must have an orientation attribute"
| ValueSome attrs ->
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = ItemsLayout.Orientation.Key) attrs with
| None -> failwith "LinearItemsLayout must have an orientation attribute"
| Some attr -> SmallScalars.IntEnum.decode<ItemsLayoutOrientation> attr.NumericValue

LinearItemsLayout(orientation)
)

let ItemSpacing = Attributes.defineBindableWithEquality<float> LinearItemsLayout.ItemSpacingProperty

[<AutoOpen>]
module LinearItemsBuilders =
type Fabulous.XamarinForms.View with
static member inline LinearItemsLayout(orientation: ItemsLayoutOrientation) =
WidgetBuilder<'msg, ILinearItemsLayout>(
LinearItemsLayout.WidgetKey,
ItemsLayout.Orientation.WithValue(orientation)
)

[<Extension>]
type LinearItemsLayoutExtensions =
[<Extension>]
static member inline itemSpacing(this: WidgetBuilder<'msg, #ILinearItemsLayout>, value: float) =
this.AddScalar(LinearItemsLayout.ItemSpacing.WithValue(value))

/// <summary>Link a ViewRef to access the direct LinearItemsLayout control instance</summary>
[<Extension>]
static member inline reference(this: WidgetBuilder<'msg, IListView>, value: ViewRef<LinearItemsLayout>) =
this.AddScalar(ViewRefAttributes.ViewRef.WithValue(value.Unbox))
38 changes: 38 additions & 0 deletions src/Fabulous.XamarinForms/Views/Collections/_GroupableItemsView.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Fabulous.XamarinForms

open Fabulous
open Fabulous.XamarinForms
open Xamarin.Forms

type IGroupableItemsView = inherit ISelectableItemsView

module GroupableItemsView =
let GroupedItemsSource =
Attributes.defineSimpleScalar<GroupedWidgetItems>
"CollectionView_GroupedItemsSource"
(fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems)
(fun _ newValueOpt node ->
let collectionView = node.Target :?> GroupableItemsView

match newValueOpt with
| ValueNone ->
collectionView.IsGrouped <- false
collectionView.ClearValue(CollectionView.ItemsSourceProperty)
collectionView.ClearValue(CollectionView.GroupHeaderTemplateProperty)
collectionView.ClearValue(CollectionView.GroupFooterTemplateProperty)
collectionView.ClearValue(CollectionView.ItemTemplateProperty)

| ValueSome value ->
collectionView.IsGrouped <- true

collectionView.SetValue(CollectionView.ItemTemplateProperty, WidgetDataTemplateSelector(node, unbox >> value.ItemTemplate))

collectionView.SetValue(CollectionView.GroupHeaderTemplateProperty, WidgetDataTemplateSelector(node, unbox >> value.HeaderTemplate))

if value.FooterTemplate.IsSome then
collectionView.SetValue(
CollectionView.GroupFooterTemplateProperty,
WidgetDataTemplateSelector(node, unbox >> value.FooterTemplate.Value)
)

collectionView.SetValue(CollectionView.ItemsSourceProperty, value.OriginalItems))
Loading

0 comments on commit 3cd1735

Please sign in to comment.