diff --git a/src/Fabulous.Tests/APISketchTests.fs b/src/Fabulous.Tests/APISketchTests.fs index 8e7ee5a7a..cbd1a82e4 100644 --- a/src/Fabulous.Tests/APISketchTests.fs +++ b/src/Fabulous.Tests/APISketchTests.fs @@ -150,7 +150,7 @@ module SimpleStackTests = // add first instance.ProcessMessage(AddNew(1, "yo")) - Assert.AreEqual(stack.Children.Count, 1) + Assert.AreEqual(1, stack.Children.Count) let label = stack.Children.[0] :?> TestLabel :> IText diff --git a/src/Fabulous.Tests/TestUI.Attributes.fs b/src/Fabulous.Tests/TestUI.Attributes.fs index 6ce45dbe8..7a7c7bf11 100644 --- a/src/Fabulous.Tests/TestUI.Attributes.fs +++ b/src/Fabulous.Tests/TestUI.Attributes.fs @@ -15,7 +15,7 @@ module Attributes = ConvertValue = id Compare = ScalarAttributeComparers.noCompare UpdateNode = - fun (newValueOpt, node) -> + fun newValueOpt node -> let btn = node.Target :?> IButton diff --git a/src/Fabulous.Tests/TestUI.ViewUpdaters.fs b/src/Fabulous.Tests/TestUI.ViewUpdaters.fs index e9a93ee19..9731565e7 100644 --- a/src/Fabulous.Tests/TestUI.ViewUpdaters.fs +++ b/src/Fabulous.Tests/TestUI.ViewUpdaters.fs @@ -37,19 +37,19 @@ open Tests.Platform // navigationPage.PushAsync(page) |> ignore -let updateText (newValueOpt: string voption, node: IViewNode) = +let updateText (newValueOpt: string voption) (node: IViewNode) = let textElement = node.Target :?> IText textElement.Text <- ValueOption.defaultValue "" newValueOpt -let updateRecord (newValueOpt: bool voption, node: IViewNode) = +let updateRecord (newValueOpt: bool voption) (node: IViewNode) = let textElement = node.Target :?> TestLabel textElement.record <- ValueOption.defaultValue false newValueOpt -let updateTextColor (newValueOpt: string voption, node: IViewNode) = +let updateTextColor (newValueOpt: string voption) (node: IViewNode) = let textElement = node.Target :?> IText textElement.TextColor <- ValueOption.defaultValue "" newValueOpt -let updateAutomationId (newValueOpt: string voption, node: IViewNode) = +let updateAutomationId (newValueOpt: string voption) (node: IViewNode) = let el = node.Target :?> TestViewElement el.AutomationId <- ValueOption.defaultValue "" newValueOpt diff --git a/src/Fabulous.Tests/TestUI.Widgets.fs b/src/Fabulous.Tests/TestUI.Widgets.fs index e151c1580..b294b4f99 100644 --- a/src/Fabulous.Tests/TestUI.Widgets.fs +++ b/src/Fabulous.Tests/TestUI.Widgets.fs @@ -29,7 +29,7 @@ module Widgets = CreateView = fun (widget, context, parentNode) -> let name = typeof<'T>.Name - printfn $"Creating view for {name}" + // printfn $"Creating view for {name}" let view = new 'T() let weakReference = WeakReference(view) diff --git a/src/Fabulous.XamarinForms/Attributes.fs b/src/Fabulous.XamarinForms/Attributes.fs index ccd13262e..e8e442c2d 100644 --- a/src/Fabulous.XamarinForms/Attributes.fs +++ b/src/Fabulous.XamarinForms/Attributes.fs @@ -30,14 +30,14 @@ module Attributes = (bindableProperty: BindableProperty) (convert: 'inputType -> 'modelType) (convertValue: 'modelType -> 'valueType) - (compare: 'modelType * 'modelType -> ScalarAttributeComparison) + (compare: 'modelType -> 'modelType -> ScalarAttributeComparison) = Attributes.defineScalarWithConverter<'inputType, 'modelType, 'valueType> bindableProperty.PropertyName convert convertValue compare - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let target = node.Target :?> BindableObject match newValueOpt with @@ -53,7 +53,7 @@ module Attributes = id id ScalarAttributeComparers.equalityCompare - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let target = node.Target :?> BindableObject match newValueOpt with diff --git a/src/Fabulous.XamarinForms/ViewUpdaters.fs b/src/Fabulous.XamarinForms/ViewUpdaters.fs index fe170c143..7f7a18cc6 100644 --- a/src/Fabulous.XamarinForms/ViewUpdaters.fs +++ b/src/Fabulous.XamarinForms/ViewUpdaters.fs @@ -4,7 +4,7 @@ open Fabulous open Xamarin.Forms module ViewUpdaters = - let updateSliderMinMax (newValueOpt: (float * float) voption, node: IViewNode) = + let updateSliderMinMax (newValueOpt: struct (float * float) voption) (node: IViewNode) = let slider = node.Target :?> Slider match newValueOpt with @@ -22,7 +22,7 @@ module ViewUpdaters = slider.SetValue(Slider.MinimumProperty, min) slider.SetValue(Slider.MaximumProperty, max) - let updateStepperMinMax (newValueOpt: (float * float) voption, node: IViewNode) = + let updateStepperMinMax (newValueOpt: struct (float * float) voption) (node: IViewNode) = let stepper = node.Target :?> Stepper match newValueOpt with @@ -40,7 +40,7 @@ module ViewUpdaters = stepper.SetValue(Stepper.MinimumProperty, min) stepper.SetValue(Stepper.MaximumProperty, max) - let updateGridColumnDefinitions (newValueOpt: Dimension [] voption, node: IViewNode) = + let updateGridColumnDefinitions (newValueOpt: Dimension [] voption) (node: IViewNode) = let grid = node.Target :?> Grid match newValueOpt with @@ -58,7 +58,7 @@ module ViewUpdaters = grid.ColumnDefinitions.Add(ColumnDefinition(Width = gridLength)) - let updateGridRowDefinitions (newValueOpt: Dimension [] voption, node: IViewNode) = + let updateGridRowDefinitions (newValueOpt: Dimension [] voption) (node: IViewNode) = let grid = node.Target :?> Grid match newValueOpt with @@ -78,11 +78,11 @@ module ViewUpdaters = /// NOTE: Would be better to have a custom diff logic for Navigation /// because it's a Stack and not a random access collection - let applyDiffNavigationPagePages (diffs: ArraySlice, node: IViewNode) = + let applyDiffNavigationPagePages (diffs: WidgetCollectionItemChanges) (node: IViewNode) = let navigationPage = node.Target :?> NavigationPage let pages = List.ofSeq navigationPage.Pages - for diff in ArraySlice.toSpan diffs do + for diff in diffs do match diff with | WidgetCollectionItemChange.Insert (index, widget) -> if index >= pages.Length then @@ -110,17 +110,7 @@ module ViewUpdaters = let childNode = node.TreeContext.GetViewNode(box pages.[index]) - match diff.ScalarChanges with - | ValueSome changes -> childNode.ApplyScalarDiffs(changes) - | ValueNone -> () - - match diff.WidgetChanges with - | ValueSome slice -> childNode.ApplyWidgetDiffs(ArraySlice.toSpan slice) - | ValueNone -> () - - match diff.WidgetCollectionChanges with - | ValueSome slice -> childNode.ApplyWidgetCollectionDiffs(ArraySlice.toSpan slice) - | ValueNone -> () + childNode.ApplyDiff(&diff) | WidgetCollectionItemChange.Replace (index, widget) -> @@ -163,7 +153,7 @@ module ViewUpdaters = navigationPage.PushAsync(temp.Pop(), false) |> ignore - let updateNavigationPagePages (newValueOpt: ArraySlice voption, node: IViewNode) = + let updateNavigationPagePages (newValueOpt: ArraySlice voption) (node: IViewNode) = let navigationPage = node.Target :?> NavigationPage navigationPage.PopToRootAsync(false) |> ignore diff --git a/src/Fabulous.XamarinForms/WidgetExtensions.fs b/src/Fabulous.XamarinForms/WidgetExtensions.fs index 8a97ae58d..c8df39a41 100644 --- a/src/Fabulous.XamarinForms/WidgetExtensions.fs +++ b/src/Fabulous.XamarinForms/WidgetExtensions.fs @@ -14,7 +14,7 @@ module AdditionalAttributes = let UseSafeArea = Attributes.define "Page_UseSafeArea" - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let page = node.Target :?> Xamarin.Forms.Page let value = @@ -28,7 +28,7 @@ module AdditionalAttributes = let ToolbarPlacement = Attributes.define "TabbedPage_ToolbarPlacement" - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let tabbedPage = node.Target :?> Xamarin.Forms.TabbedPage let value = diff --git a/src/Fabulous.XamarinForms/Xamarin.Forms.Core.Attributes.fs b/src/Fabulous.XamarinForms/Xamarin.Forms.Core.Attributes.fs index 7865eeab1..b89f630cb 100644 --- a/src/Fabulous.XamarinForms/Xamarin.Forms.Core.Attributes.fs +++ b/src/Fabulous.XamarinForms/Xamarin.Forms.Core.Attributes.fs @@ -15,7 +15,7 @@ module Application = let Resources = Attributes.define "Application_Resources" - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let application = node.Target :?> Xamarin.Forms.Application @@ -29,7 +29,7 @@ module Application = let UserAppTheme = Attributes.define "Application_UserAppTheme" - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let application = node.Target :?> Xamarin.Forms.Application @@ -241,7 +241,7 @@ module Switch = module Slider = let MinimumMaximum = - Attributes.define "Slider_MinimumMaximum" ViewUpdaters.updateSliderMinMax + Attributes.define "Slider_MinimumMaximum" ViewUpdaters.updateSliderMinMax let Value = Attributes.defineBindable Xamarin.Forms.Slider.ValueProperty @@ -399,7 +399,7 @@ module ToolbarItem = let Order = Attributes.define "ToolbarItem_Order" - (fun (newValueOpt, node) -> + (fun newValueOpt node -> let toolbarItem = node.Target :?> Xamarin.Forms.ToolbarItem @@ -495,7 +495,7 @@ module Stepper = Attributes.defineBindable Xamarin.Forms.Stepper.IncrementProperty let MinimumMaximum = - Attributes.define "Stepper_MinimumMaximum" ViewUpdaters.updateStepperMinMax + Attributes.define "Stepper_MinimumMaximum" ViewUpdaters.updateStepperMinMax let Value = Attributes.defineBindable Xamarin.Forms.Stepper.ValueProperty @@ -515,7 +515,7 @@ module ItemsView = for x in modelValue.OriginalItems do modelValue.Template x }) - (fun (a, b) -> ScalarAttributeComparers.equalityCompare (a.OriginalItems, b.OriginalItems)) + (fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems) let GroupedItemsSource<'T> = Attributes.defineBindableWithComparer, GroupedWidgetItems<'T>, IEnumerable> @@ -526,7 +526,7 @@ module ItemsView = for x in modelValue.OriginalItems do modelValue.Template x }) - (fun (a, b) -> ScalarAttributeComparers.equalityCompare (a.OriginalItems, b.OriginalItems)) + (fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems) module ItemsViewOfCell = let ItemsSource<'T> = @@ -539,7 +539,7 @@ module ItemsViewOfCell = for x in modelValue.OriginalItems do modelValue.Template x }) - (fun (a, b) -> ScalarAttributeComparers.equalityCompare (a.OriginalItems, b.OriginalItems)) + (fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems) let GroupedItemsSource<'T> = Attributes.defineBindableWithComparer, GroupedWidgetItems<'T>, IEnumerable> @@ -551,7 +551,7 @@ module ItemsViewOfCell = for x in modelValue.OriginalItems do modelValue.Template x }) - (fun (a, b) -> ScalarAttributeComparers.equalityCompare (a.OriginalItems, b.OriginalItems)) + (fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems) module ListView = let RowHeight = diff --git a/src/Fabulous.XamarinForms/Xamarin.Forms.Core.fs b/src/Fabulous.XamarinForms/Xamarin.Forms.Core.fs index e9856825c..fe7d565a4 100644 --- a/src/Fabulous.XamarinForms/Xamarin.Forms.Core.fs +++ b/src/Fabulous.XamarinForms/Xamarin.Forms.Core.fs @@ -256,7 +256,7 @@ type ViewBuilders private () = ViewKeys.Slider, Slider.Value.WithValue(value), Slider.ValueChanged.WithValue(fun args -> onValueChanged args.NewValue |> box), - Slider.MinimumMaximum.WithValue(min, max) + Slider.MinimumMaximum.WithValue(struct (min, max)) ) static member inline ActivityIndicator<'msg>(isRunning: bool) = @@ -398,7 +398,7 @@ type ViewBuilders private () = ViewKeys.Stepper, Stepper.Value.WithValue(value), Stepper.ValueChanged.WithValue(fun args -> onValueChanged args.NewValue |> box), - Stepper.MinimumMaximum.WithValue((min, max)) + Stepper.MinimumMaximum.WithValue(struct (min, max)) ) static member inline ListView<'msg, 'itemData, 'itemMarker when 'itemMarker :> ICell>(items: seq<'itemData>) = diff --git a/src/Fabulous/AttributeDefinitions.fs b/src/Fabulous/AttributeDefinitions.fs index b5ec369dd..6deddd5b1 100644 --- a/src/Fabulous/AttributeDefinitions.fs +++ b/src/Fabulous/AttributeDefinitions.fs @@ -1,20 +1,16 @@ namespace Fabulous -open Fabulous open System.Collections.Generic +open Fabulous type IAttributeDefinition = abstract member Key: AttributeKey - abstract member UpdateNode: newValueOpt: obj voption * node: IViewNode -> unit - -[] -type ScalarAttributeComparison = - | Identical - | Different + abstract member UpdateNode: obj voption -> IViewNode -> unit type IScalarAttributeDefinition = inherit IAttributeDefinition - abstract member CompareBoxed: a: obj * b: obj -> ScalarAttributeComparison + abstract member CompareBoxed: a: obj -> b: obj -> ScalarAttributeComparison + /// Attribute definition for scalar properties type ScalarAttributeDefinition<'inputType, 'modelType, 'valueType> = @@ -22,8 +18,8 @@ type ScalarAttributeDefinition<'inputType, 'modelType, 'valueType> = Name: string Convert: 'inputType -> 'modelType ConvertValue: 'modelType -> 'valueType - Compare: 'modelType * 'modelType -> ScalarAttributeComparison - UpdateNode: 'valueType voption * IViewNode -> unit } + Compare: 'modelType -> 'modelType -> ScalarAttributeComparison + UpdateNode: 'valueType voption -> IViewNode -> unit } member x.WithValue(value) : ScalarAttribute = { Key = x.Key @@ -35,23 +31,23 @@ type ScalarAttributeDefinition<'inputType, 'modelType, 'valueType> = interface IScalarAttributeDefinition with member x.Key = x.Key - member x.CompareBoxed(a, b) = - x.Compare(unbox<'modelType> a, unbox<'modelType> b) + member x.CompareBoxed a b = + x.Compare(unbox<'modelType> a) (unbox<'modelType> b) - member x.UpdateNode(newValueOpt, node) = + member x.UpdateNode newValueOpt node = let newValueOpt = match newValueOpt with | ValueNone -> ValueNone | ValueSome v -> ValueSome(x.ConvertValue(unbox<'modelType> v)) - x.UpdateNode(newValueOpt, node) + x.UpdateNode newValueOpt node /// Attribute definition for widget properties type WidgetAttributeDefinition = { Key: AttributeKey Name: string - ApplyDiff: WidgetDiff * IViewNode -> unit - UpdateNode: Widget voption * IViewNode -> unit } + ApplyDiff: WidgetDiff -> IViewNode -> unit + UpdateNode: Widget voption -> IViewNode -> unit } member x.WithValue(value: Widget) : WidgetAttribute = { Key = x.Key @@ -63,20 +59,20 @@ type WidgetAttributeDefinition = interface IAttributeDefinition with member x.Key = x.Key - member x.UpdateNode(newValueOpt, node) = + member x.UpdateNode newValueOpt node = let newValueOpt = match newValueOpt with | ValueNone -> ValueNone | ValueSome v -> ValueSome(unbox v) - x.UpdateNode(newValueOpt, node) + x.UpdateNode newValueOpt node /// Attribute definition for collection properties type WidgetCollectionAttributeDefinition = { Key: AttributeKey Name: string - ApplyDiff: ArraySlice * IViewNode -> unit - UpdateNode: ArraySlice voption * IViewNode -> unit } + ApplyDiff: WidgetCollectionItemChanges -> IViewNode -> unit + UpdateNode: ArraySlice voption -> IViewNode -> unit } member x.WithValue(value: ArraySlice) : WidgetCollectionAttribute = { Key = x.Key @@ -88,13 +84,13 @@ type WidgetCollectionAttributeDefinition = interface IAttributeDefinition with member x.Key = x.Key - member x.UpdateNode(newValueOpt, node) = + member x.UpdateNode newValueOpt node = let newValueOpt = match newValueOpt with | ValueNone -> ValueNone | ValueSome v -> ValueSome(unbox> v) - x.UpdateNode(newValueOpt, node) + x.UpdateNode newValueOpt node module AttributeDefinitionStore = let private _attributes = diff --git a/src/Fabulous/Attributes.fs b/src/Fabulous/Attributes.fs index 4f6cf2ed5..42641e530 100644 --- a/src/Fabulous/Attributes.fs +++ b/src/Fabulous/Attributes.fs @@ -15,9 +15,9 @@ module Helpers = view module ScalarAttributeComparers = - let noCompare (_, _) = ScalarAttributeComparison.Different + let noCompare _ _ = ScalarAttributeComparison.Different - let equalityCompare (a, b) = + let equalityCompare a b = if a = b then ScalarAttributeComparison.Identical else @@ -29,8 +29,8 @@ module Attributes = name (convert: 'inputType -> 'modelType) (convertValue: 'modelType -> 'valueType) - (compare: 'modelType * 'modelType -> ScalarAttributeComparison) - (updateNode: 'valueType voption * IViewNode -> unit) + (compare: 'modelType -> 'modelType -> ScalarAttributeComparison) + (updateNode: 'valueType voption -> IViewNode -> unit) = let key = AttributeDefinitionStore.getNextKey () @@ -48,8 +48,8 @@ module Attributes = /// Define a custom attribute storing a widget let defineWidgetWithConverter name - (applyDiff: WidgetDiff * IViewNode -> unit) - (updateNode: Widget voption * IViewNode -> unit) + (applyDiff: WidgetDiff -> IViewNode -> unit) + (updateNode: Widget voption -> IViewNode -> unit) = let key = AttributeDefinitionStore.getNextKey () @@ -65,8 +65,8 @@ module Attributes = /// Define a custom attribute storing a widget collection let defineWidgetCollectionWithConverter name - (applyDiff: ArraySlice * IViewNode -> unit) - (updateNode: ArraySlice voption * IViewNode -> unit) + (applyDiff: WidgetCollectionItemChanges -> IViewNode -> unit) + (updateNode: ArraySlice voption -> IViewNode -> unit) = let key = AttributeDefinitionStore.getNextKey () @@ -81,22 +81,13 @@ module Attributes = /// Define an attribute storing a Widget for a CLR property let defineWidget<'T when 'T: null> (name: string) (get: obj -> IViewNode) (set: obj -> 'T -> unit) = - let applyDiff (diff: WidgetDiff, node: IViewNode) = + let applyDiff (diff: WidgetDiff) (node: IViewNode) = let childNode = get node.Target - match diff.ScalarChanges with - | ValueSome changes -> childNode.ApplyScalarDiffs(changes) - | ValueNone -> () - - match diff.WidgetChanges with - | ValueSome slice -> childNode.ApplyWidgetDiffs(ArraySlice.toSpan slice) - | ValueNone -> () + childNode.ApplyDiff(&diff) - match diff.WidgetCollectionChanges with - | ValueSome slice -> childNode.ApplyWidgetCollectionDiffs(ArraySlice.toSpan slice) - | ValueNone -> () - let updateNode (newValueOpt: Widget voption, node: IViewNode) = + let updateNode (newValueOpt: Widget voption) (node: IViewNode) = match newValueOpt with | ValueNone -> set node.Target null | ValueSome widget -> @@ -109,15 +100,15 @@ module Attributes = /// Define an attribute storing a collection of Widget let defineWidgetCollection<'itemType> name (getCollection: obj -> System.Collections.Generic.IList<'itemType>) = - let applyDiff (diffs: ArraySlice, node: IViewNode) = + let applyDiff (diffs: WidgetCollectionItemChanges) (node: IViewNode) = let targetColl = getCollection node.Target - for diff in ArraySlice.toSpan diffs do + for diff in diffs do match diff with | WidgetCollectionItemChange.Remove index -> targetColl.RemoveAt(index) | _ -> () - for diff in ArraySlice.toSpan diffs do + for diff in diffs do match diff with | WidgetCollectionItemChange.Insert (index, widget) -> let view = Helpers.createViewForWidget node widget @@ -127,17 +118,7 @@ module Attributes = let childNode = node.TreeContext.GetViewNode(box targetColl.[index]) - match widgetDiff.ScalarChanges with - | ValueSome changes -> childNode.ApplyScalarDiffs(changes) - | ValueNone -> () - - match widgetDiff.WidgetChanges with - | ValueSome slice -> childNode.ApplyWidgetDiffs(ArraySlice.toSpan slice) - | ValueNone -> () - - match widgetDiff.WidgetCollectionChanges with - | ValueSome slice -> childNode.ApplyWidgetCollectionDiffs(ArraySlice.toSpan slice) - | ValueNone -> () + childNode.ApplyDiff(&widgetDiff) | WidgetCollectionItemChange.Replace (index, widget) -> let view = Helpers.createViewForWidget node widget @@ -145,7 +126,7 @@ module Attributes = | _ -> () - let updateNode (newValueOpt: ArraySlice voption, node: IViewNode) = + let updateNode (newValueOpt: ArraySlice voption) (node: IViewNode) = let targetColl = getCollection node.Target targetColl.Clear() @@ -191,7 +172,7 @@ module Attributes = ConvertValue = id Compare = ScalarAttributeComparers.noCompare UpdateNode = - fun (newValueOpt, node) -> + fun newValueOpt node -> let event = getEvent node.Target match node.TryGetHandler(key) with @@ -221,7 +202,7 @@ module Attributes = ConvertValue = id Compare = ScalarAttributeComparers.noCompare UpdateNode = - fun (newValueOpt: ('args -> obj) voption, node: IViewNode) -> + fun (newValueOpt: ('args -> obj) voption) (node: IViewNode) -> let event = getEvent node.Target match node.TryGetHandler(key) with diff --git a/src/Fabulous/Core.fs b/src/Fabulous/Core.fs deleted file mode 100644 index bd8147402..000000000 --- a/src/Fabulous/Core.fs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Fabulous - -open System - -/// Dev notes: -/// -/// The types in this file will be the ones used the most internally by Fabulous. -/// -/// To enable the best performance possible, we want to avoid allocating them on -/// the heap as must as possible (meaning they should be structs where possible) -/// Also we want to avoid cache line misses, in that end, we make sure each struct -/// can fit on a L1/L2 cache size by making those structs fit on 64 bits. -/// -/// Having those performance constraints prevents us for using inheritance -/// or using interfaces on these structs - -type AttributeKey = int -type WidgetKey = int -type StateKey = int -type ViewAdapterKey = int - - -/// Represents a value for a property of a widget. -/// Can map to a real property (such as Label.Text) or to a non-existent one. -/// It will be up to the AttributeDefinition to decide how to apply the value. -[] -type ScalarAttribute = - { Key: AttributeKey -#if DEBUG - DebugName: string -#endif - Value: obj } - - -and [] WidgetAttribute = - { Key: AttributeKey -#if DEBUG - DebugName: string -#endif - Value: Widget } - -and [] WidgetCollectionAttribute = - { Key: AttributeKey -#if DEBUG - DebugName: string -#endif - Value: ArraySlice } - -/// Represents a virtual UI element such as a Label, a Button, etc. -and [] Widget = - { Key: WidgetKey -#if DEBUG - DebugName: string -#endif - ScalarAttributes: ScalarAttribute [] voption - WidgetAttributes: WidgetAttribute [] voption - WidgetCollectionAttributes: WidgetCollectionAttribute [] voption } - -[] -type ScalarChange = - | Added of added: ScalarAttribute - | Removed of removed: ScalarAttribute - | Updated of updated: ScalarAttribute - -and [] WidgetChange = - | Added of added: WidgetAttribute - | Removed of removed: WidgetAttribute - | Updated of updated: struct (WidgetAttribute * WidgetDiff) - | ReplacedBy of replacedBy: WidgetAttribute - -and [] WidgetCollectionChange = - | Added of added: WidgetCollectionAttribute - | Removed of removed: WidgetCollectionAttribute - | Updated of updated: struct (WidgetCollectionAttribute * ArraySlice) - -and [] WidgetCollectionItemChange = - | Insert of widgetInserted: struct (int * Widget) - | Replace of widgetReplaced: struct (int * Widget) - | Update of widgetUpdated: struct (int * WidgetDiff) - | Remove of removed: int - -and [] WidgetDiff = - { ScalarChanges: ScalarChange [] voption - WidgetChanges: ArraySlice voption - WidgetCollectionChanges: ArraySlice voption } - -/// Context of the whole view tree -[] -type ViewTreeContext = - { CanReuseView: Widget -> Widget -> bool - GetViewNode: obj -> IViewNode - Dispatch: obj -> unit } - -and IViewNode = - abstract member Target: obj - abstract member Parent: IViewNode voption - abstract member TreeContext: ViewTreeContext - abstract member MapMsg: (obj -> obj) voption with get, set - - // note that Widget is struct type, thus we have boxing via option - // we don't have MemoizedWidget set for 99.9% of the cases - // thus makes sense to have overhead of boxing - // in order to save space - abstract member MemoizedWidget: Widget option with get, set - abstract member TryGetHandler<'T> : AttributeKey -> 'T voption - abstract member SetHandler<'T> : AttributeKey * 'T voption -> unit - abstract member ApplyScalarDiffs: ScalarChange [] -> unit - abstract member ApplyWidgetDiffs: Span -> unit - abstract member ApplyWidgetCollectionDiffs: Span -> unit diff --git a/src/Fabulous/Fabulous.fsproj b/src/Fabulous/Fabulous.fsproj index 915a6734b..86bee2d29 100644 --- a/src/Fabulous/Fabulous.fsproj +++ b/src/Fabulous/Fabulous.fsproj @@ -7,26 +7,28 @@ https://github.com/TimLariviere/Fabulous-new - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + \ No newline at end of file diff --git a/src/Fabulous/IViewNode.fs b/src/Fabulous/IViewNode.fs new file mode 100644 index 000000000..945c7cd55 --- /dev/null +++ b/src/Fabulous/IViewNode.fs @@ -0,0 +1,25 @@ +namespace Fabulous + + +/// Context of the whole view tree +[] +type ViewTreeContext = + { CanReuseView: Widget -> Widget -> bool + GetViewNode: obj -> IViewNode + Dispatch: obj -> unit } + +and IViewNode = + abstract member Target: obj + abstract member Parent: IViewNode voption + abstract member TreeContext: ViewTreeContext + abstract member MapMsg: (obj -> obj) voption with get, set + + // note that Widget is struct type, thus we have boxing via option + // we don't have MemoizedWidget set for 99.9% of the cases + // thus makes sense to have overhead of boxing + // in order to save space + abstract member MemoizedWidget: Widget option with get, set + abstract member TryGetHandler<'T> : AttributeKey -> 'T voption + abstract member SetHandler<'T> : AttributeKey * 'T voption -> unit + + abstract member ApplyDiff: WidgetDiff inref -> unit diff --git a/src/Fabulous/MapMsg.fs b/src/Fabulous/MapMsg.fs index 655e0073c..9e97cdf8c 100644 --- a/src/Fabulous/MapMsg.fs +++ b/src/Fabulous/MapMsg.fs @@ -7,7 +7,7 @@ module MapMsg = id id ScalarAttributeComparers.noCompare - (fun (value, node) -> + (fun value node -> match value with | ValueNone -> node.MapMsg <- ValueNone | ValueSome fn -> node.MapMsg <- ValueSome fn) diff --git a/src/Fabulous/Memo.fs b/src/Fabulous/Memo.fs index e6f03a3b8..f152d87dc 100644 --- a/src/Fabulous/Memo.fs +++ b/src/Fabulous/Memo.fs @@ -35,7 +35,7 @@ module Memo = let internal canReuseMemoizedWidget prev next = (getMemoData prev).MarkerType = (getMemoData next).MarkerType - let private compareAttributes (prev: MemoData, next: MemoData) : ScalarAttributeComparison = + let private compareAttributes (prev: MemoData) (next: MemoData) : ScalarAttributeComparison = match (prev.KeyType = next.KeyType, prev.MarkerType = next.MarkerType) with | true, true -> match next.KeyComparer next.KeyData prev.KeyData with @@ -43,7 +43,7 @@ module Memo = | false -> ScalarAttributeComparison.Different | _ -> ScalarAttributeComparison.Different - let private updateNode (data: MemoData voption, node: IViewNode) : unit = + let private updateNode (data: MemoData voption) (node: IViewNode) : unit = match data with | ValueSome memoData -> let memoizedWidget = memoData.CreateWidget memoData.KeyData @@ -72,10 +72,12 @@ module Memo = + // Memo isn't allowed in lists, TargetType will never get called, + // so Unchecked.defaultof is an acceptable value let private widgetDefinition: WidgetDefinition = { Key = MemoWidgetKey Name = "Memo" - TargetType = Unchecked.defaultof<_> // Memo isn't allowed in lists, so this will never get called + TargetType = Unchecked.defaultof<_> CreateView = fun (widget, context, parentNode) -> diff --git a/src/Fabulous/Primitives.fs b/src/Fabulous/Primitives.fs new file mode 100644 index 000000000..b814ddc1e --- /dev/null +++ b/src/Fabulous/Primitives.fs @@ -0,0 +1,57 @@ +namespace Fabulous + +open Fabulous + +/// Dev notes: +/// +/// The types in this file will be the ones used the most internally by Fabulous. +/// +/// To enable the best performance possible, we want to avoid allocating them on +/// the heap as must as possible (meaning they should be structs where possible) +/// Also we want to avoid cache line misses, in that end, we make sure each struct +/// can fit on a L1/L2 cache size by making those structs fit on 64 bits. +/// +/// Having those performance constraints prevents us for using inheritance +/// or using interfaces on these structs + +type AttributeKey = int +type WidgetKey = int +type StateKey = int +type ViewAdapterKey = int + + +/// Represents a value for a property of a widget. +/// Can map to a real property (such as Label.Text) or to a non-existent one. +/// It will be up to the AttributeDefinition to decide how to apply the value. +[] +type ScalarAttribute = + { Key: AttributeKey +#if DEBUG + DebugName: string +#endif + Value: obj } + + +and [] WidgetAttribute = + { Key: AttributeKey +#if DEBUG + DebugName: string +#endif + Value: Widget } + +and [] WidgetCollectionAttribute = + { Key: AttributeKey +#if DEBUG + DebugName: string +#endif + Value: ArraySlice } + +/// Represents a virtual UI element such as a Label, a Button, etc. +and [] Widget = + { Key: WidgetKey +#if DEBUG + DebugName: string +#endif + ScalarAttributes: ScalarAttribute [] voption + WidgetAttributes: WidgetAttribute [] voption + WidgetCollectionAttributes: WidgetCollectionAttribute [] voption } diff --git a/src/Fabulous/Reconciler.fs b/src/Fabulous/Reconciler.fs index f14fad6fd..67583f7f3 100644 --- a/src/Fabulous/Reconciler.fs +++ b/src/Fabulous/Reconciler.fs @@ -1,394 +1,23 @@ namespace Fabulous open Fabulous -open Fabulous.StackAllocatedCollections module Reconciler = - /// Let's imagine that we have the following situation - /// prev = [|1,2,6,7|] note that it is sorted - /// next = [|8,5,6,2|] unsorted - /// In reality we have Key and Value, but let's pretend that we only care about keys for now - /// - /// then the desired outcome is this - /// added = [5, 8] - /// removed = [1, 7] - /// - /// Approach - /// 1. we sort both arrays - /// prev = [|1,2,6,7|] - /// next = [|2,5,6,8|] - /// - /// 2. Starting from index 0 for both of the arrays - /// if prevItem < nextItem then - /// inc prevIndex, add prevItem to removed, goto 2 - /// if prevItem > nextItem - /// inc nextIndex, add nextItem to added, goto 2 - /// else (meaning equals) - /// compare values - /// - /// break when we reached both ends of the arrays - let rec diffScalarAttributes - (prev: ScalarAttribute [] voption) - (next: ScalarAttribute [] voption) - : ScalarChange [] voption = - match (prev, next) with - | ValueNone, ValueNone -> ValueNone - // all were deleted - | ValueSome prev, ValueNone -> - prev - |> Array.map ScalarChange.Removed - |> ValueSome - | ValueNone, ValueSome next -> next |> Array.map ScalarChange.Added |> ValueSome - | ValueSome prev, ValueSome next -> + let private compareScalars (struct (key, a, b): struct (AttributeKey * obj * obj)) : ScalarAttributeComparison = + let def = + AttributeDefinitionStore.get key :?> IScalarAttributeDefinition - let mutable result = DiffBuilder.create () + def.CompareBoxed a b - let mutable prevIndex = 0 - let mutable nextIndex = 0 - - let prevLength = prev.Length - let nextLength = next.Length - - while not (prevIndex >= prevLength && nextIndex >= nextLength) do - if prevIndex = prevLength then - // that means we are done with the prev and only need to add next's tail to added - //result <- StackArray3.add(&result, (ScalarChange.Added next.[nextIndex])) - DiffBuilder.addOpMut &result DiffBuilder.Add (uint16 nextIndex) - nextIndex <- nextIndex + 1 - - elif nextIndex = nextLength then - // that means that we are done with new items and only need prev's tail to removed - // result <- StackArray3.add(&result, ScalarChange.Removed prev.[prevIndex]) - DiffBuilder.addOpMut &result DiffBuilder.Remove (uint16 prevIndex) - prevIndex <- prevIndex + 1 - - else - // we haven't reached either of the ends - let prevAttr = prev.[prevIndex] - let nextAttr = next.[nextIndex] - - let prevKey = prevAttr.Key - let nextKey = nextAttr.Key - - match prevKey.CompareTo nextKey with - | c when c < 0 -> - // prev key is less than next -> remove prev key - DiffBuilder.addOpMut &result DiffBuilder.Remove (uint16 prevIndex) - // result <- StackArray3.add(&result, ScalarChange.Removed prevAttr) - prevIndex <- prevIndex + 1 - - | c when c > 0 -> - // prev key is more than next -> add next item - // result <- StackArray3.add(&result, ScalarChange.Added nextAttr) - DiffBuilder.addOpMut &result DiffBuilder.Add (uint16 nextIndex) - nextIndex <- nextIndex + 1 - - | _ -> - // means that we are targeting the same attribute - - let definition = - AttributeDefinitionStore.get prevAttr.Key :?> IScalarAttributeDefinition - - match definition.CompareBoxed(prevAttr.Value, nextAttr.Value) with - // Previous and next values are identical, we don't need to do anything - | ScalarAttributeComparison.Identical -> () - - // New value completely replaces the old value - | ScalarAttributeComparison.Different -> - DiffBuilder.addOpMut &result DiffBuilder.Change (uint16 nextIndex) - - // move both pointers - prevIndex <- prevIndex + 1 - nextIndex <- nextIndex + 1 - - - match DiffBuilder.lenght &result with - | 0 -> ValueNone - | _ -> - ValueSome( - DiffBuilder.toArray - &result - (fun op -> - match op with - | DiffBuilder.Added i -> ScalarChange.Added next.[int i] - | DiffBuilder.Removed i -> ScalarChange.Removed prev.[int i] - | DiffBuilder.Changed i -> ScalarChange.Updated next.[int i]) - ) - - and diffWidgetAttributes - (canReuseView: Widget -> Widget -> bool) - (prev: WidgetAttribute [] voption) - (next: WidgetAttribute [] voption) - : ArraySlice voption = - - match (prev, next) with - | ValueNone, ValueNone -> ValueNone - - // all were deleted - | ValueSome prev, ValueNone -> - prev - |> Array.map WidgetChange.Removed - |> ArraySlice.fromArray - - | ValueNone, ValueSome next -> - next - |> Array.map WidgetChange.Added - |> ArraySlice.fromArray - - | ValueSome prev, ValueSome next -> - - let mutable result = MutStackArray1.Empty - - let mutable prevIndex = 0 - let mutable nextIndex = 0 - - let prevLength = prev.Length - let nextLength = next.Length - - while not (prevIndex >= prevLength && nextIndex >= nextLength) do - if prevIndex = prevLength then - // that means we are done with the prev and only need to add next's tail to added - result <- MutStackArray1.addMut (&result, WidgetChange.Added next.[nextIndex]) - nextIndex <- nextIndex + 1 - - elif nextIndex = nextLength then - // that means that we are done with new items and only need prev's tail to removed - result <- MutStackArray1.addMut (&result, WidgetChange.Removed prev.[prevIndex]) - prevIndex <- prevIndex + 1 - - else - // we haven't reached either of the ends - let prevAttr = prev.[prevIndex] - let nextAttr = next.[nextIndex] - - let prevKey = prevAttr.Key - let nextKey = nextAttr.Key - let prevWidget = prevAttr.Value - let nextWidget = nextAttr.Value - - match prevKey.CompareTo nextKey with - | c when c < 0 -> - // prev key is less than next -> remove prev key - result <- MutStackArray1.addMut (&result, WidgetChange.Removed prevAttr) - prevIndex <- prevIndex + 1 - - | c when c > 0 -> - // prev key is more than next -> add next item - result <- MutStackArray1.addMut (&result, WidgetChange.Added nextAttr) - nextIndex <- nextIndex + 1 - - | _ -> - // means that we are targeting the same attribute - - // move both pointers - prevIndex <- prevIndex + 1 - nextIndex <- nextIndex + 1 - - let changeOpt = - if prevWidget = nextWidget then - ValueNone - elif canReuseView prevWidget nextWidget then - match diffWidget canReuseView (ValueSome prevWidget) nextWidget with - | ValueNone -> ValueNone - | ValueSome diffs -> ValueSome(WidgetChange.Updated struct (nextAttr, diffs)) - else - ValueSome(WidgetChange.ReplacedBy nextAttr) - - match changeOpt with - | ValueNone -> () - | ValueSome change -> result <- MutStackArray1.addMut (&result, change) - - MutStackArray1.toArraySlice &result - - - and diffWidgetCollectionAttributes - (canReuseView: Widget -> Widget -> bool) - (prev: WidgetCollectionAttribute [] voption) - (next: WidgetCollectionAttribute [] voption) - : ArraySlice voption = - - match (prev, next) with - | ValueNone, ValueNone -> ValueNone - - // all were deleted - | ValueSome prev, ValueNone -> - prev - |> Array.map WidgetCollectionChange.Removed - |> ArraySlice.fromArray - - | ValueNone, ValueSome next -> - next - |> Array.map WidgetCollectionChange.Added - |> ArraySlice.fromArray - - | ValueSome prev, ValueSome next -> - - let mutable result = MutStackArray1.Empty - - - let mutable prevIndex = 0 - let mutable nextIndex = 0 - - let prevLength = prev.Length - let nextLength = next.Length - - while not (prevIndex >= prevLength && nextIndex >= nextLength) do - if prevIndex = prevLength then - // that means we are done with the prev and only need to add next's tail to added - // DiffBuilder.addOpMut &result DiffBuilder.Add (uint16 nextIndex) - result <- MutStackArray1.addMut (&result, WidgetCollectionChange.Added next.[nextIndex]) - - - nextIndex <- nextIndex + 1 - - elif nextIndex = nextLength then - // that means that we are done with new items and only need prev's tail to removed - // DiffBuilder.addOpMut &result DiffBuilder.Remove (uint16 prevIndex) - result <- MutStackArray1.addMut (&result, WidgetCollectionChange.Removed prev.[prevIndex]) - - - prevIndex <- prevIndex + 1 - - else - // we haven't reached either of the ends - let prevAttr = prev.[prevIndex] - let nextAttr = next.[nextIndex] - - let prevKey = prevAttr.Key - let nextKey = nextAttr.Key - let prevWidgetColl = prevAttr.Value - let nextWidgetColl = nextAttr.Value - - match prevKey.CompareTo nextKey with - | c when c < 0 -> - // prev key is less than next -> remove prev key - - result <- MutStackArray1.addMut (&result, WidgetCollectionChange.Removed prevAttr) - prevIndex <- prevIndex + 1 - - | c when c > 0 -> - // prev key is more than next -> add next item - result <- MutStackArray1.addMut (&result, WidgetCollectionChange.Added nextAttr) - nextIndex <- nextIndex + 1 - - | _ -> - // means that we are targeting the same attribute - - // move both pointers - prevIndex <- prevIndex + 1 - nextIndex <- nextIndex + 1 - - let diff = - diffWidgetCollections canReuseView prevWidgetColl nextWidgetColl - - match diff with - | ValueNone -> () - | ValueSome slice -> - let change = - WidgetCollectionChange.Updated struct (nextAttr, slice) - - result <- MutStackArray1.addMut (&result, change) - - MutStackArray1.toArraySlice &result - - and diffWidgetCollections - (canReuseView: Widget -> Widget -> bool) - (prev: ArraySlice) - (next: ArraySlice) - : ArraySlice voption = - let mutable result = MutStackArray1.Empty - - let prev = ArraySlice.toSpan prev - let next = ArraySlice.toSpan next - - if prev.Length > next.Length then - for i = next.Length to prev.Length - 1 do - result <- MutStackArray1.addMut (&result, WidgetCollectionItemChange.Remove i) - - for i = 0 to next.Length - 1 do - let currItem = next.[i] - // index < 0 || index >= array.Length ? (FSharpOption) null : FSharpOption.Some(array[index]); - let prevItemOpt = - if (i >= prev.Length) then - ValueNone - else - ValueSome prev.[i] - - let changeOpt = - match prevItemOpt with - | ValueNone -> ValueSome(WidgetCollectionItemChange.Insert struct (i, currItem)) - - | ValueSome prevItem when canReuseView prevItem currItem -> - - match diffWidget canReuseView (ValueSome prevItem) currItem with - | ValueNone -> ValueNone - | ValueSome diffs -> ValueSome(WidgetCollectionItemChange.Update struct (i, diffs)) - - | ValueSome _ -> ValueSome(WidgetCollectionItemChange.Replace struct (i, currItem)) - - match changeOpt with - | ValueNone -> () - | ValueSome change -> result <- MutStackArray1.addMut (&result, change) - - MutStackArray1.toArraySlice &result - - and diffWidget - (canReuseView: Widget -> Widget -> bool) - (prevOpt: Widget voption) - (next: Widget) - : WidgetDiff voption = - let prevScalarAttributes = - match prevOpt with - | ValueNone -> ValueNone - | ValueSome widget -> widget.ScalarAttributes - - let prevWidgetAttributes = - match prevOpt with - | ValueNone -> ValueNone - | ValueSome widget -> widget.WidgetAttributes - - let prevWidgetCollectionAttributes = - match prevOpt with - | ValueNone -> ValueNone - | ValueSome widget -> widget.WidgetCollectionAttributes - - let scalarDiffs = - diffScalarAttributes prevScalarAttributes next.ScalarAttributes - - let widgetDiffs = - diffWidgetAttributes canReuseView prevWidgetAttributes next.WidgetAttributes - - let collectionDiffs = - diffWidgetCollectionAttributes canReuseView prevWidgetCollectionAttributes next.WidgetCollectionAttributes - - - match (scalarDiffs, widgetDiffs, collectionDiffs) with - | ValueNone, ValueNone, ValueNone -> ValueNone - | _ -> - ValueSome - { ScalarChanges = scalarDiffs - WidgetChanges = widgetDiffs - WidgetCollectionChanges = collectionDiffs } - - /// Diffs changes and applies them on the target let update (canReuseView: Widget -> Widget -> bool) (prevOpt: Widget voption) (next: Widget) (node: IViewNode) : unit = - match diffWidget canReuseView prevOpt next with - | ValueNone -> () - | ValueSome diff -> - match diff.ScalarChanges with - | ValueSome changes -> node.ApplyScalarDiffs(changes) - | ValueNone -> () - match diff.WidgetChanges with - | ValueSome slice -> node.ApplyWidgetDiffs(ArraySlice.toSpan slice) - | ValueNone -> () + let diff = + WidgetDiff.create (prevOpt, next, canReuseView, compareScalars) - match diff.WidgetCollectionChanges with - | ValueSome slice -> node.ApplyWidgetCollectionDiffs(ArraySlice.toSpan slice) - | ValueNone -> () + node.ApplyDiff(&diff) diff --git a/src/Fabulous/Runners.fs b/src/Fabulous/Runners.fs index 38253203c..3b90ec539 100644 --- a/src/Fabulous/Runners.fs +++ b/src/Fabulous/Runners.fs @@ -144,7 +144,6 @@ module ViewAdapters = let node = getViewNode _root - // TODO handle the case when Type of the widget changes Reconciler.update canReuseView (ValueSome prevWidget) currentWidget node _allowDispatch <- true diff --git a/src/Fabulous/ViewNode.fs b/src/Fabulous/ViewNode.fs index 73598fdcc..beeae5fbd 100644 --- a/src/Fabulous/ViewNode.fs +++ b/src/Fabulous/ViewNode.fs @@ -3,6 +3,7 @@ open Fabulous /// Define the logic to apply diffs and store event handlers of its target control +[] type ViewNode(parentNode: IViewNode voption, treeContext: ViewTreeContext, targetRef: System.WeakReference) = // TODO consider combine handlers mapMsg and property bag @@ -10,6 +11,70 @@ type ViewNode(parentNode: IViewNode voption, treeContext: ViewTreeContext, targe // ViewNode is supposed to be mutable, stateful and persistent object let mutable _handlers: Map = Map.empty + member inline private this.ApplyScalarDiffs(diffs: ScalarChanges inref) = + for diff in diffs do + match diff with + | ScalarChange.Added added -> + let definition = + AttributeDefinitionStore.get added.Key :?> IScalarAttributeDefinition + + definition.UpdateNode(ValueSome added.Value) this + + | ScalarChange.Removed removed -> + let definition = + AttributeDefinitionStore.get removed.Key :?> IScalarAttributeDefinition + + definition.UpdateNode ValueNone this + + | ScalarChange.Updated newAttr -> + let definition = + AttributeDefinitionStore.get newAttr.Key :?> IScalarAttributeDefinition + + definition.UpdateNode(ValueSome newAttr.Value) this + + member inline private this.ApplyWidgetDiffs(diffs: WidgetChanges inref) = + for diff in diffs do + match diff with + | WidgetChange.Added newWidget + | WidgetChange.ReplacedBy newWidget -> + let definition = + AttributeDefinitionStore.get newWidget.Key :?> WidgetAttributeDefinition + + definition.UpdateNode(ValueSome newWidget.Value) (this :> IViewNode) + + | WidgetChange.Removed removed -> + let definition = + AttributeDefinitionStore.get removed.Key :?> WidgetAttributeDefinition + + definition.UpdateNode ValueNone (this :> IViewNode) + + | WidgetChange.Updated struct (newAttr, diffs) -> + let definition = + AttributeDefinitionStore.get newAttr.Key :?> WidgetAttributeDefinition + + definition.ApplyDiff diffs (this :> IViewNode) + + member inline private this.ApplyWidgetCollectionDiffs(diffs: WidgetCollectionChanges inref) = + for diff in diffs do + match diff with + | WidgetCollectionChange.Added added -> + let definition = + AttributeDefinitionStore.get added.Key :?> WidgetCollectionAttributeDefinition + + definition.UpdateNode(ValueSome added.Value) (this :> IViewNode) + + | WidgetCollectionChange.Removed removed -> + let definition = + AttributeDefinitionStore.get removed.Key :?> WidgetCollectionAttributeDefinition + + definition.UpdateNode ValueNone (this :> IViewNode) + + | WidgetCollectionChange.Updated struct (newAttr, diffs) -> + let definition = + AttributeDefinitionStore.get newAttr.Key :?> WidgetCollectionAttributeDefinition + + definition.ApplyDiff diffs (this :> IViewNode) + interface IViewNode with member _.Target = targetRef.Target member _.TreeContext = treeContext @@ -32,75 +97,11 @@ type ViewNode(parentNode: IViewNode voption, treeContext: ViewTreeContext, targe | ValueNone -> None | ValueSome h -> Some(box h)) - member this.ApplyScalarDiffs(diffs) = - if not targetRef.IsAlive then - () - else - for diff in diffs do - match diff with - | ScalarChange.Added added -> - let definition = - AttributeDefinitionStore.get added.Key :?> IScalarAttributeDefinition - - definition.UpdateNode(ValueSome added.Value, this) - - | ScalarChange.Removed removed -> - let definition = - AttributeDefinitionStore.get removed.Key :?> IScalarAttributeDefinition - - definition.UpdateNode(ValueNone, this) - - | ScalarChange.Updated newAttr -> - let definition = - AttributeDefinitionStore.get newAttr.Key :?> IScalarAttributeDefinition - - definition.UpdateNode(ValueSome newAttr.Value, this) - - member this.ApplyWidgetDiffs(diffs) = - if not targetRef.IsAlive then - () - else - for diff in diffs do - match diff with - | WidgetChange.Added newWidget - | WidgetChange.ReplacedBy newWidget -> - let definition = - AttributeDefinitionStore.get newWidget.Key :?> WidgetAttributeDefinition - - definition.UpdateNode(ValueSome newWidget.Value, this :> IViewNode) - - | WidgetChange.Removed removed -> - let definition = - AttributeDefinitionStore.get removed.Key :?> WidgetAttributeDefinition - - definition.UpdateNode(ValueNone, this :> IViewNode) - - | WidgetChange.Updated struct (newAttr, diffs) -> - let definition = - AttributeDefinitionStore.get newAttr.Key :?> WidgetAttributeDefinition - - definition.ApplyDiff(diffs, this :> IViewNode) - member this.ApplyWidgetCollectionDiffs(diffs) = + member x.ApplyDiff(diff) = if not targetRef.IsAlive then () else - for diff in diffs do - match diff with - | WidgetCollectionChange.Added added -> - let definition = - AttributeDefinitionStore.get added.Key :?> WidgetCollectionAttributeDefinition - - definition.UpdateNode(ValueSome added.Value, this :> IViewNode) - - | WidgetCollectionChange.Removed removed -> - let definition = - AttributeDefinitionStore.get removed.Key :?> WidgetCollectionAttributeDefinition - - definition.UpdateNode(ValueNone, this :> IViewNode) - - | WidgetCollectionChange.Updated struct (newAttr, diffs) -> - let definition = - AttributeDefinitionStore.get newAttr.Key :?> WidgetCollectionAttributeDefinition - - definition.ApplyDiff(diffs, this :> IViewNode) + x.ApplyScalarDiffs(&diff.ScalarChanges) + x.ApplyWidgetDiffs(&diff.WidgetChanges) + x.ApplyWidgetCollectionDiffs(&diff.WidgetCollectionChanges) diff --git a/src/Fabulous/WidgetDiff.fs b/src/Fabulous/WidgetDiff.fs new file mode 100644 index 000000000..a4e271d6e --- /dev/null +++ b/src/Fabulous/WidgetDiff.fs @@ -0,0 +1,525 @@ +namespace Fabulous + +open System +open System.Runtime.CompilerServices +open Fabulous + +[] +type ScalarAttributeComparison = + | Identical + | Different + +[] +type EnumerationMode<'a> = + | AllAddedOrRemoved of struct ('a [] * bool) + | Empty + | ActualDiff of prevNext: struct ('a [] * 'a []) + +module EnumerationMode = + let fromOptions prev next = + match prev, next with + | ValueNone, ValueNone -> EnumerationMode.Empty + | ValueSome prev, ValueNone -> EnumerationMode.AllAddedOrRemoved(prev, false) + | ValueNone, ValueSome next -> EnumerationMode.AllAddedOrRemoved(next, true) + | ValueSome prev, ValueSome next -> EnumerationMode.ActualDiff(prev, next) + +[] +type ScalarChange = + | Added of added: ScalarAttribute + | Removed of removed: ScalarAttribute + | Updated of updated: ScalarAttribute + +and [] WidgetChange = + | Added of added: WidgetAttribute + | Removed of removed: WidgetAttribute + | Updated of updated: struct (WidgetAttribute * WidgetDiff) + | ReplacedBy of replacedBy: WidgetAttribute + +and [] WidgetCollectionChange = + | Added of added: WidgetCollectionAttribute + | Removed of removed: WidgetCollectionAttribute + | Updated of updated: struct (WidgetCollectionAttribute * WidgetCollectionItemChanges) + +and [] WidgetCollectionItemChange = + | Insert of widgetInserted: struct (int * Widget) + | Replace of widgetReplaced: struct (int * Widget) + | Update of widgetUpdated: struct (int * WidgetDiff) + | Remove of removed: int + +and [] WidgetDiff = + { ScalarChanges: ScalarChanges + WidgetChanges: WidgetChanges + WidgetCollectionChanges: WidgetCollectionChanges } + + static member inline create + ( + prevOpt: Widget voption, + next: Widget, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) : WidgetDiff = + + let prevScalarAttributes = + match prevOpt with + | ValueNone -> ValueNone + | ValueSome widget -> widget.ScalarAttributes + + let prevWidgetAttributes = + match prevOpt with + | ValueNone -> ValueNone + | ValueSome widget -> widget.WidgetAttributes + + let prevWidgetCollectionAttributes = + match prevOpt with + | ValueNone -> ValueNone + | ValueSome widget -> widget.WidgetCollectionAttributes + + { ScalarChanges = ScalarChanges(prevScalarAttributes, next.ScalarAttributes, compareScalars) + WidgetChanges = WidgetChanges(prevWidgetAttributes, next.WidgetAttributes, canReuseView, compareScalars) + WidgetCollectionChanges = + WidgetCollectionChanges( + prevWidgetCollectionAttributes, + next.WidgetCollectionAttributes, + canReuseView, + compareScalars + ) } + +and [] ScalarChanges + ( + prev: ScalarAttribute [] voption, + next: ScalarAttribute [] voption, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + member _.GetEnumerator() = + ScalarChangesEnumerator(EnumerationMode.fromOptions prev next, compareScalars) + +and [] WidgetChanges + ( + prev: WidgetAttribute [] voption, + next: WidgetAttribute [] voption, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + member _.GetEnumerator() = + WidgetChangesEnumerator(EnumerationMode.fromOptions prev next, canReuseView, compareScalars) + +and [] WidgetCollectionChanges + ( + prev: WidgetCollectionAttribute [] voption, + next: WidgetCollectionAttribute [] voption, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + member _.GetEnumerator() = + WidgetCollectionChangesEnumerator(EnumerationMode.fromOptions prev next, canReuseView, compareScalars) + + +and [] WidgetCollectionItemChanges + ( + prev: ArraySlice, + next: ArraySlice, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + member _.GetEnumerator() = + WidgetCollectionItemChangesEnumerator( + ArraySlice.toSpan prev, + ArraySlice.toSpan next, + canReuseView, + compareScalars + ) + +// enumerators +and [] ScalarChangesEnumerator + ( + mode: EnumerationMode, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + + [] + val mutable private current: ScalarChange + + [] + val mutable private prevIndex: int + + [] + val mutable private nextIndex: int + + member e.Current = e.current + + member e.MoveNext() = + match mode with + | EnumerationMode.Empty -> false + | EnumerationMode.AllAddedOrRemoved (attributes, added) -> + // use prevIndex regardless if it is for adding or removal + let i = e.prevIndex + + if i < attributes.Length then + let attribute = attributes.[i] + + e.current <- + match added with + | false -> ScalarChange.Removed attribute + | true -> ScalarChange.Added attribute + + e.prevIndex <- i + 1 + true + else + false + + | EnumerationMode.ActualDiff (prev, next) -> + let mutable prevIndex = e.prevIndex + let mutable nextIndex = e.nextIndex + + let prevLength = prev.Length + let nextLength = next.Length + + let mutable res: bool voption = ValueNone + // that needs to be in a loop until we have a change + + while ValueOption.isNone res do + + if not (prevIndex >= prevLength && nextIndex >= nextLength) then + if prevIndex = prevLength then + // that means we are done with the prev and only need to add next's tail to added + e.current <- ScalarChange.Added next.[nextIndex] + res <- ValueSome true + nextIndex <- nextIndex + 1 + + elif nextIndex = nextLength then + // that means that we are done with new items and only need prev's tail to removed + e.current <- ScalarChange.Removed prev.[prevIndex] + res <- ValueSome true + prevIndex <- prevIndex + 1 + + else + // we haven't reached either of the ends + let prevAttr = prev.[prevIndex] + let nextAttr = next.[nextIndex] + + let prevKey = prevAttr.Key + let nextKey = nextAttr.Key + + match prevKey.CompareTo nextKey with + | c when c < 0 -> + // prev key is less than next -> remove prev key + e.current <- ScalarChange.Removed prev.[prevIndex] + res <- ValueSome true + prevIndex <- prevIndex + 1 + + | c when c > 0 -> + // prev key is more than next -> add next item + e.current <- ScalarChange.Added prev.[nextIndex] + res <- ValueSome true + nextIndex <- nextIndex + 1 + + | _ -> + // means that we are targeting the same attribute + + match compareScalars struct (prevKey, prevAttr.Value, nextAttr.Value) with + // Previous and next values are identical, we don't need to do anything + | ScalarAttributeComparison.Identical -> () + + // New value completely replaces the old value + | ScalarAttributeComparison.Different -> + e.current <- ScalarChange.Updated next.[nextIndex] + res <- ValueSome true + + // move both pointers + prevIndex <- prevIndex + 1 + nextIndex <- nextIndex + 1 + + else + res <- ValueSome false + + e.prevIndex <- prevIndex + e.nextIndex <- nextIndex + + match res with + | ValueNone -> false + | ValueSome res -> res + +and [] WidgetChangesEnumerator + ( + mode: EnumerationMode, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + + [] + val mutable private current: WidgetChange + + [] + val mutable private prevIndex: int + + [] + val mutable private nextIndex: int + + member e.Current = e.current + + member e.MoveNext() = + match mode with + | EnumerationMode.Empty -> false + | EnumerationMode.AllAddedOrRemoved (struct (values, added)) -> + // use prevIndex regardless if it is for adding or removal + let i = e.prevIndex + + if i < values.Length then + let value = values.[i] + + e.current <- + match added with + | false -> WidgetChange.Removed value + | true -> WidgetChange.Added value + + e.prevIndex <- i + 1 + true + else + false + + | EnumerationMode.ActualDiff (struct (prev, next)) -> + let mutable prevIndex = e.prevIndex + let mutable nextIndex = e.nextIndex + + let prevLength = prev.Length + let nextLength = next.Length + + let mutable res: bool voption = ValueNone + // that needs to be in a loop until we have a change + + while ValueOption.isNone res do + if not (prevIndex >= prevLength && nextIndex >= nextLength) then + if prevIndex = prevLength then + // that means we are done with the prev and only need to add next's tail to added + e.current <- WidgetChange.Added next.[nextIndex] + res <- ValueSome true + nextIndex <- nextIndex + 1 + + elif nextIndex = nextLength then + // that means that we are done with new items and only need prev's tail to removed + e.current <- WidgetChange.Removed prev.[prevIndex] + res <- ValueSome true + prevIndex <- prevIndex + 1 + + else + // we haven't reached either of the ends + let prevAttr = prev.[prevIndex] + let nextAttr = next.[nextIndex] + + let prevKey = prevAttr.Key + let nextKey = nextAttr.Key + let prevWidget = prevAttr.Value + let nextWidget = nextAttr.Value + + match prevKey.CompareTo nextKey with + | c when c < 0 -> + // prev key is less than next -> remove prev key + e.current <- WidgetChange.Removed prevAttr + res <- ValueSome true + + prevIndex <- prevIndex + 1 + + | c when c > 0 -> + // prev key is more than next -> add next item + e.current <- WidgetChange.Added nextAttr + res <- ValueSome true + nextIndex <- nextIndex + 1 + + | _ -> + // means that we are targeting the same attribute + + // move both pointers + prevIndex <- prevIndex + 1 + nextIndex <- nextIndex + 1 + + if prevWidget <> nextWidget then + + let change = + if canReuseView prevWidget nextWidget then + let diff = + WidgetDiff.create ( + (ValueSome prevWidget), + nextWidget, + canReuseView, + compareScalars + ) + + WidgetChange.Updated(nextAttr, diff) + else + WidgetChange.ReplacedBy nextAttr + + e.current <- change + res <- ValueSome true + + else + res <- ValueSome false + + e.prevIndex <- prevIndex + e.nextIndex <- nextIndex + + match res with + | ValueNone -> false + | ValueSome res -> res + +and [] WidgetCollectionChangesEnumerator + ( + mode: EnumerationMode, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + + [] + val mutable private current: WidgetCollectionChange + + [] + val mutable private prevIndex: int + + [] + val mutable private nextIndex: int + + member e.Current = e.current + + member e.MoveNext() = + match mode with + | EnumerationMode.Empty -> false + | EnumerationMode.AllAddedOrRemoved (struct (values, added)) -> + // use prevIndex regardless if it is for adding or removal + let i = e.prevIndex + + if i < values.Length then + let value = values.[i] + + e.current <- + match added with + | false -> WidgetCollectionChange.Removed value + | true -> WidgetCollectionChange.Added value + + e.prevIndex <- i + 1 + true + else + false + + | EnumerationMode.ActualDiff (struct (prev, next)) -> + let mutable prevIndex = e.prevIndex + let mutable nextIndex = e.nextIndex + + let prevLength = prev.Length + let nextLength = next.Length + + // that needs to be in a loop until we have a change + + let res = + if not (prevIndex >= prevLength && nextIndex >= nextLength) then + if prevIndex = prevLength then + // that means we are done with the prev and only need to add next's tail to added + e.current <- WidgetCollectionChange.Added next.[nextIndex] + nextIndex <- nextIndex + 1 + + elif nextIndex = nextLength then + // that means that we are done with new items and only need prev's tail to removed + e.current <- WidgetCollectionChange.Removed prev.[prevIndex] + prevIndex <- prevIndex + 1 + else + // we haven't reached either of the ends + let prevAttr = prev.[prevIndex] + let nextAttr = next.[nextIndex] + + let prevKey = prevAttr.Key + let nextKey = nextAttr.Key + let prevWidgetColl = prevAttr.Value + let nextWidgetColl = nextAttr.Value + + match prevKey.CompareTo nextKey with + | c when c < 0 -> + // prev key is less than next -> remove prev key + e.current <- WidgetCollectionChange.Removed prevAttr + prevIndex <- prevIndex + 1 + + | c when c > 0 -> + // prev key is more than next -> add next item + e.current <- WidgetCollectionChange.Added nextAttr + nextIndex <- nextIndex + 1 + + | _ -> + // means that we are targeting the same attribute + + // move both pointers + prevIndex <- prevIndex + 1 + nextIndex <- nextIndex + 1 + + let diff = + WidgetCollectionItemChanges( + prevWidgetColl, + nextWidgetColl, + canReuseView, + compareScalars + ) + + e.current <- WidgetCollectionChange.Updated(nextAttr, diff) + + true + else + false + + e.prevIndex <- prevIndex + e.nextIndex <- nextIndex + + res + +and [] WidgetCollectionItemChangesEnumerator + ( + prev: Span, + next: Span, + canReuseView: Widget -> Widget -> bool, + compareScalars: struct (AttributeKey * obj * obj) -> ScalarAttributeComparison + ) = + [] + val mutable private current: WidgetCollectionItemChange + + [] + val mutable private index: int + + [] + val mutable private tailIndex: int + + member e.Current = e.current + + member e.MoveNext() = + let tailIndex = e.tailIndex + let i = e.index + + if prev.Length > next.Length + && tailIndex < prev.Length - next.Length then + + e.current <- WidgetCollectionItemChange.Remove(next.Length - tailIndex) + e.tailIndex <- tailIndex + 1 + + true + + elif i < next.Length then + let currItem = next.[i] + + let prevItemOpt = + if (i >= prev.Length) then + ValueNone + else + ValueSome prev.[i] + + match prevItemOpt with + | ValueNone -> e.current <- WidgetCollectionItemChange.Insert(i, currItem) + + | ValueSome prevItem when canReuseView prevItem currItem -> + + let diff = + WidgetDiff.create (ValueSome prevItem, currItem, canReuseView, compareScalars) + + e.current <- WidgetCollectionItemChange.Update(i, diff) + + | ValueSome _ -> e.current <- WidgetCollectionItemChange.Replace(i, currItem) + + e.index <- i + 1 + true + + else + // means that we are done iterating + false