diff --git a/CHANGELOG.md b/CHANGELOG.md index e4806300b..c037e3bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 _No unreleased changes_ +## [2.5.0-pre3] - 2024-01-16 + +### Changed +- Consolidation of the new Component API and the existing ViewAdapter by @TimLariviere (https://github.com/fabulous-dev/Fabulous/pull/1056) + +## [2.5.0-pre2] - 2023-11-22 + +### Changed +- Couple of changes to the new Component API + ## [2.5.0-pre1] - 2023-11-22 ### Added @@ -55,8 +65,10 @@ _No unreleased changes_ ### Changed - Fabulous.XamarinForms & Fabulous.MauiControls have been moved been out of the Fabulous repository. Find them in their own repositories: [https://github.com/fabulous-dev/Fabulous.XamarinForms](https://github.com/fabulous-dev/Fabulous.XamarinForms) / [https://github.com/fabulous-dev/Fabulous.MauiControls](https://github.com/fabulous-dev/Fabulous.MauiControls) -[unreleased]: https://github.com/fabulous-dev/Fabulous/compare/2.5.0-pre1...HEAD -[2.5.0-preview.1]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.5.0-pre1 +[unreleased]: https://github.com/fabulous-dev/Fabulous/compare/2.5.0-pre3...HEAD +[2.5.0-pre3]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.5.0-pre3 +[2.5.0-pre2]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.5.0-pre2 +[2.5.0-pre1]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.5.0-pre1 [2.4.0]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.4.0 [2.3.2]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.3.2 [2.3.1]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.3.1 diff --git a/Directory.Build.props b/Directory.Build.props index 257c87297..0b9322c2b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,4 +18,9 @@ snupkg + + $(OtherFlags) --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen + true + + \ No newline at end of file diff --git a/src/Fabulous.Benchmarks/Benchmarks.fs b/src/Fabulous.Benchmarks/Benchmarks.fs index e51fd675a..34ad3cadf 100644 --- a/src/Fabulous.Benchmarks/Benchmarks.fs +++ b/src/Fabulous.Benchmarks/Benchmarks.fs @@ -1,7 +1,6 @@ module Fabulous.Tests.Benchmarks open BenchmarkDotNet.Attributes -open BenchmarkDotNet.Jobs open BenchmarkDotNet.Running open Fabulous.Tests.APISketchTests.TestUI_Widgets @@ -95,7 +94,7 @@ module DiffingAttributes = let instance = Run.Instance program - let _tree = (instance.Start()) + let _tree = instance.Start() for i in 1..100 do instance.ProcessMessage(IncBy i) @@ -171,7 +170,7 @@ module DiffingSmallScalars = let instance = Run.Instance program - let _tree = (instance.Start()) + let _tree = instance.Start() for i in 1..100 do instance.ProcessMessage(IncBy 1UL) @@ -179,7 +178,7 @@ module DiffingSmallScalars = [] -let main argv = +let main _argv = // BenchmarkRunner.Run() // |> ignore // diff --git a/src/Fabulous.Tests/APISketchTests/APISketchTests.fs b/src/Fabulous.Tests/APISketchTests/APISketchTests.fs index c3c8b83b7..733bb4571 100644 --- a/src/Fabulous.Tests/APISketchTests/APISketchTests.fs +++ b/src/Fabulous.Tests/APISketchTests/APISketchTests.fs @@ -4,7 +4,6 @@ open Fabulous.StackAllocatedCollections open Fabulous.Tests.APISketchTests.Platform open NUnit.Framework -open Platform open TestUI_Widgets open Fabulous @@ -75,7 +74,7 @@ module ButtonTests = let update msg model = match msg with - | Increment -> { model with count = model.count + 1 } + | Increment -> { count = model.count + 1 } let view model = @@ -137,7 +136,7 @@ module SimpleStackTests = instance.ProcessMessage(AddNew(1, "yo")) Assert.AreEqual(1, stack.Children.Count) - let label = stack.Children.[0] :?> TestLabel :> IText + let label = stack.Children[0] :?> TestLabel :> IText Assert.AreEqual(label.Text, "yo") @@ -145,14 +144,14 @@ module SimpleStackTests = instance.ProcessMessage(AddNew(2, "yo2")) Assert.AreEqual(2, stack.Children.Count) - let label = stack.Children.[0] :?> TestLabel :> IText + let label = stack.Children[0] :?> TestLabel :> IText Assert.AreEqual(label.Text, "yo2") // modify the initial one instance.ProcessMessage(ChangeText(1, "just 1")) - let label = stack.Children.[1] :?> TestLabel :> IText + let label = stack.Children[1] :?> TestLabel :> IText Assert.AreEqual(label.Text, "just 1") @@ -160,7 +159,7 @@ module SimpleStackTests = instance.ProcessMessage(Delete 2) Assert.AreEqual(stack.Children.Count, 1) - let label = stack.Children.[0] :?> TestLabel :> IText + let label = stack.Children[0] :?> TestLabel :> IText Assert.AreEqual(label.Text, "just 1") @@ -549,9 +548,7 @@ module SmallScalars = let update msg model = match msg with - | Inc value -> - { model with - value = model.value + value } + | Inc value -> { value = model.value + value } let view model = InlineNumericBag(model.value, model.value + 1UL, float(model.value + 2UL)) @@ -690,7 +687,7 @@ module Issue104 = module Issue1044 = [] let ``Multiple Widgets + for loops in builder causes crash`` () = - let view model = + let view _model = Stack() { Label($"Foo") // It also crashes only with the multiple for loops Label($"bar") // It also crashes only with the multiple for loops @@ -717,7 +714,7 @@ module Issue1044 = [] let ``Multiple for loops in builder causes crash`` () = - let view model = + let view _model = Stack() { for i = 0 to 10 do Label($"T{i}") @@ -815,7 +812,7 @@ module ViewHelpers = let childView = Button("Child button", ChildClick).automationId("childButton") - let parentView model = + let parentView _model = Stack() { Button("Parent button", ParentClick).automationId("parentButton") @@ -846,7 +843,7 @@ module ViewHelpers = let ``Adding property modifiers to widget converted with View.map is valid`` () = let childView = Button("Child button", ChildClick).automationId("childButton") - let parentView model = + let parentView _model = Stack() { (View.map ChildMessage childView).textColor("blue") } let init () = true @@ -867,7 +864,7 @@ module ViewHelpers = let childView = Button("Child button", ChildClick).automationId("childButton") - let parentView model = + let parentView _model = Stack() { (View.map ChildMessage childView).tap(ParentTap) } let init () = true @@ -899,7 +896,7 @@ module ViewHelpers = (View.map ChildMessage childView).tap(ParentTap) } - let grandParentView model = + let grandParentView _model = Stack() { Button("Grand Parent button", GrandParentClick) .automationId("grandParentButton") @@ -951,7 +948,7 @@ module ViewHelpers = let parentView = (View.map ChildMessage childView).tap(ParentTap) - let grandParentView model = + let grandParentView _model = (View.map ParentMessage parentView).tap2(GrandParentTap) let init () = true diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Component.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Component.fs new file mode 100644 index 000000000..88787b7c1 --- /dev/null +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Component.fs @@ -0,0 +1,12 @@ +namespace Fabulous.Tests.APISketchTests + +module TestUI_Component = + + open Fabulous + open Platform + + module Component = + let ComponentProperty = "ComponentProperty" + + let getComponent (target: obj) = + (target :?> TestViewElement).PropertyBag.Item ComponentProperty diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs index 6e23caff1..183629fba 100644 --- a/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs @@ -47,7 +47,7 @@ module Platform = interface IText with member x.Text with get () = text - and set (value) = + and set value = if x.record then x.changeList <- List.append x.changeList [ TextSet value ] @@ -55,7 +55,7 @@ module Platform = member x.TextColor with get () = textColor - and set (value) = + and set value = if x.record then x.changeList <- List.append x.changeList [ ColorSet value ] diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs index a26b43ec5..b84f2b85d 100644 --- a/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs @@ -10,6 +10,7 @@ module TestUI_Widgets = open Platform open TestUI_Attributes open TestUI_ViewNode + open TestUI_Component //----WidgetsBuilderCE--- @@ -29,7 +30,7 @@ module TestUI_Widgets = TargetType = typeof<'T> CreateView = fun (widget, context, parentNode) -> - let name = typeof<'T>.Name + // let name = typeof<'T>.Name // printfn $"Creating view for {name}" let view = new 'T() @@ -211,7 +212,8 @@ module TestUI_Widgets = Logger = { Log = fun _ -> () MinLogLevel = LogLevel.Fatal } - Dispatch = fun msg -> unbox<'msg> msg |> x.ProcessMessage } + Dispatch = fun msg -> unbox<'msg> msg |> x.ProcessMessage + GetComponent = Component.getComponent } member x.ProcessMessage(msg: 'msg) = match state with @@ -237,7 +239,7 @@ module TestUI_Widgets = () member x.Start(arg: 'arg) = - let model = (program.Init(arg)) + let model = program.Init(arg) let widget = program.View(model).Compile() let widgetDef = WidgetDefinitionStore.get widget.Key diff --git a/src/Fabulous.Tests/ArrayTests.fs b/src/Fabulous.Tests/ArrayTests.fs index 1df0ddacf..ecfc82111 100644 --- a/src/Fabulous.Tests/ArrayTests.fs +++ b/src/Fabulous.Tests/ArrayTests.fs @@ -3,7 +3,6 @@ namespace Fabulous.Tests open System open Fabulous.StackAllocatedCollections open NUnit.Framework -open Fabulous [] type ``Array tests``() = diff --git a/src/Fabulous.Tests/Fabulous.Tests.fsproj b/src/Fabulous.Tests/Fabulous.Tests.fsproj index 75566b9b0..9205b80d0 100644 --- a/src/Fabulous.Tests/Fabulous.Tests.fsproj +++ b/src/Fabulous.Tests/Fabulous.Tests.fsproj @@ -7,6 +7,7 @@ + diff --git a/src/Fabulous.Tests/ViewTests.fs b/src/Fabulous.Tests/ViewTests.fs index 5b4200349..68943493b 100644 --- a/src/Fabulous.Tests/ViewTests.fs +++ b/src/Fabulous.Tests/ViewTests.fs @@ -3,7 +3,6 @@ namespace Fabulous.Tests open Fabulous open Fabulous.StackAllocatedCollections.StackList open NUnit.Framework -open FsCheck.NUnit type ITestControl = interface diff --git a/src/Fabulous/Array.fs b/src/Fabulous/Array.fs index 8f923d9ff..21b82b928 100644 --- a/src/Fabulous/Array.fs +++ b/src/Fabulous/Array.fs @@ -42,7 +42,7 @@ module ArraySlice = // noop if we don't have enough space if (used + by <= arr.Length) then for i = used + by - 1 downto int by do - arr.[i] <- arr.[i - by] + arr[i] <- arr[i - by] arr @@ -50,7 +50,7 @@ module Array = let inline appendOne (v: 'v) (arr: 'v array) = let res = Array.zeroCreate(arr.Length + 1) Array.blit arr 0 res 0 arr.Length - res.[arr.Length] <- v + res[arr.Length] <- v res /// This is insertion sort that is O(n*n) but it performs better @@ -63,13 +63,13 @@ module Array = for i in [ 1 .. N - 1 ] do for j = i downto 1 do - let key = getKey attrs.[j] - let prevKey = getKey attrs.[j - 1] + let key = getKey attrs[j] + let prevKey = getKey attrs[j - 1] if key < prevKey then - let temp = attrs.[j] - attrs.[j] <- attrs.[j - 1] - attrs.[j - 1] <- temp + let temp = attrs[j] + attrs[j] <- attrs[j - 1] + attrs[j - 1] <- temp attrs @@ -127,18 +127,18 @@ module StackAllocatedCollections = let used = match data.size % 3us with | 0us -> // copy 3 items - arr.[size - 1] <- v2 - arr.[size - 2] <- v1 - arr.[size - 3] <- v0 + arr[size - 1] <- v2 + arr[size - 2] <- v1 + arr[size - 3] <- v0 3 | 1us -> // copy 1 item - arr.[size - 1] <- v0 + arr[size - 1] <- v0 1 | 2us -> // copy 2 item - arr.[size - 1] <- v1 - arr.[size - 2] <- v0 + arr[size - 1] <- v1 + arr[size - 2] <- v0 2 | _ -> 0 @@ -149,9 +149,9 @@ module StackAllocatedCollections = match leftToCopy with | Empty -> i <- -1 | Filled((v0, v1, v2), before) -> - arr.[i] <- v2 - arr.[i - 1] <- v1 - arr.[i - 2] <- v0 + arr[i] <- v2 + arr[i - 1] <- v1 + arr[i - 2] <- v0 i <- i - 3 leftToCopy <- before @@ -305,7 +305,7 @@ module StackAllocatedCollections = | 1 -> v1 | _ -> v2 - | Many arr -> arr.[index] + | Many arr -> arr[index] let find (test: 'v -> bool) (arr: StackArray3<'v> inref) : 'v = @@ -417,7 +417,7 @@ module StackAllocatedCollections = | Many struct (count, mutArr) -> if mutArr.Length > (int count) then // we can fit it in - mutArr.[int count] <- value + mutArr[int count] <- value Many(count + 1us, mutArr) else // in this branch we reached the capacity of the array, thus needs to grow @@ -430,7 +430,7 @@ module StackAllocatedCollections = Array.zeroCreate(grow mutArr.Length) Array.blit mutArr 0 res 0 mutArr.Length - res.[countInt] <- value + res[countInt] <- value Many(count + 1us, res) let inline toArray (arr: T<'v> inref) : 'v array = @@ -442,7 +442,7 @@ module StackAllocatedCollections = let inline fromArray (arr: 'v array) : T<'v> = match arr.Length with | 0 -> Empty - | 1 -> One arr.[0] + | 1 -> One arr[0] | size -> Many(uint16 size, arr) let inline toArraySlice (arr: T<'v> inref) : ArraySlice<'v> voption = @@ -470,7 +470,7 @@ module StackAllocatedCollections = if arr.Length >= (int used) + 1 then // it means that arr can fit one more element let arr = ArraySlice.shiftByMut &sliceB 1us - arr.[0] <- av + arr[0] <- av Many(used + 1us, arr) else // we need to allocate a new one more @@ -478,7 +478,7 @@ module StackAllocatedCollections = let newArr = Array.zeroCreate(grow arr.Length) Array.blit arr 0 newArr 1 (int used) - newArr.[0] <- av + newArr[0] <- av Many(used + 1us, newArr) | Many sliceA -> @@ -633,7 +633,7 @@ module StackAllocatedCollections = let encodedValue = ((uint16 op <<< 14) &&& opMask) let encodedValue = encodedValue ||| (index &&& valueMask) - builder.ops.[builder.cursor] <- encodedValue + builder.ops[builder.cursor] <- encodedValue builder.cursor <- builder.cursor + 1 @@ -655,6 +655,6 @@ module StackAllocatedCollections = let res = Array.zeroCreate<'t> len for i = 0 to len - 1 do - res.[i] <- map(decode builder.ops.[i]) + res[i] <- map(decode builder.ops[i]) res diff --git a/src/Fabulous/AttributeDefinitions.fs b/src/Fabulous/AttributeDefinitions.fs index 7945d6350..908301327 100644 --- a/src/Fabulous/AttributeDefinitions.fs +++ b/src/Fabulous/AttributeDefinitions.fs @@ -191,15 +191,15 @@ module AttributeDefinitionStore = let getScalar (key: ScalarAttributeKey) : ScalarAttributeData = let index = ScalarAttributeKey.getKeyValue key - _scalars.[index] + _scalars[index] let getSmallScalar (key: ScalarAttributeKey) : SmallScalarAttributeData = let index = ScalarAttributeKey.getKeyValue key - _smallScalars.[index] + _smallScalars[index] - let getWidget (key: WidgetAttributeKey) : WidgetAttributeData = _widgets.[int key] + let getWidget (key: WidgetAttributeKey) : WidgetAttributeData = _widgets[int key] - let getWidgetCollection (key: WidgetCollectionAttributeKey) : WidgetCollectionAttributeData = _widgetCollections.[int key] + let getWidgetCollection (key: WidgetCollectionAttributeKey) : WidgetCollectionAttributeData = _widgetCollections[int key] module AttributeHelpers = open ScalarAttributeDefinitions diff --git a/src/Fabulous/Attributes.fs b/src/Fabulous/Attributes.fs index 9ef9519cd..5f377bffa 100644 --- a/src/Fabulous/Attributes.fs +++ b/src/Fabulous/Attributes.fs @@ -1,4 +1,4 @@ -namespace Fabulous +namespace Fabulous open System open System.Runtime.CompilerServices @@ -213,7 +213,7 @@ module Attributes = for diff in diffs do match diff with | WidgetCollectionItemChange.Remove(index, widget) -> - let itemNode = node.TreeContext.GetViewNode(box targetColl.[index]) + let itemNode = node.TreeContext.GetViewNode(box targetColl[index]) // Trigger the unmounted event Dispatcher.dispatchEventForAllChildren itemNode widget Lifecycle.Unmounted @@ -236,12 +236,12 @@ module Attributes = Dispatcher.dispatchEventForAllChildren itemNode widget Lifecycle.Mounted | WidgetCollectionItemChange.Update(index, widgetDiff) -> - let childNode = node.TreeContext.GetViewNode(box targetColl.[index]) + let childNode = node.TreeContext.GetViewNode(box targetColl[index]) childNode.ApplyDiff(&widgetDiff) | WidgetCollectionItemChange.Replace(index, oldWidget, newWidget) -> - let prevItemNode = node.TreeContext.GetViewNode(box targetColl.[index]) + let prevItemNode = node.TreeContext.GetViewNode(box targetColl[index]) let struct (nextItemNode, view) = Helpers.createViewForWidget node newWidget @@ -250,7 +250,7 @@ module Attributes = prevItemNode.Disconnect() // Replace the existing child in the UI tree at the index with the new one - targetColl.[index] <- unbox view + targetColl[index] <- unbox view // Trigger the mounted event for the new child Dispatcher.dispatchEventForAllChildren nextItemNode newWidget Lifecycle.Mounted diff --git a/src/Fabulous/Builders.fs b/src/Fabulous/Builders.fs index 936861bf0..e9c883252 100644 --- a/src/Fabulous/Builders.fs +++ b/src/Fabulous/Builders.fs @@ -1,6 +1,7 @@ namespace Fabulous open System.ComponentModel +open Fabulous.WidgetAttributeDefinitions open Fabulous.WidgetCollectionAttributeDefinitions open Fabulous.StackAllocatedCollections open Fabulous.StackAllocatedCollections.StackList @@ -91,7 +92,7 @@ type WidgetBuilder<'msg, 'marker> = | ValueSome attribs -> let attribs2 = Array.zeroCreate(attribs.Length + 1) Array.blit attribs 0 attribs2 0 attribs.Length - attribs2.[attribs.Length] <- attr + attribs2[attribs.Length] <- attr attribs2 WidgetBuilder<'msg, 'marker>(x.Key, struct (scalarAttributes, ValueSome res, widgetCollectionAttributes)) @@ -109,7 +110,7 @@ type WidgetBuilder<'msg, 'marker> = | ValueSome attribs -> let attribs2 = Array.zeroCreate(attribs.Length + 1) Array.blit attribs 0 attribs2 0 attribs.Length - attribs2.[attribs.Length] <- attr + attribs2[attribs.Length] <- attr attribs2 WidgetBuilder<'msg, 'marker>(x.Key, struct (scalarAttributes, widgetAttributes, ValueSome res)) @@ -217,3 +218,51 @@ type AttributeCollectionBuilder<'msg, 'marker, 'itemMarker> = res end + +type SingleChildBuilderStep<'msg, 'marker> = delegate of unit -> WidgetBuilder<'msg, 'marker> + +[] +type SingleChildBuilder<'msg, 'marker, 'childMarker> = + val WidgetKey: WidgetKey + val Attr: WidgetAttributeDefinition + val AttributesBundle: AttributesBundle + + new(widgetKey: WidgetKey, attr: WidgetAttributeDefinition) = + { WidgetKey = widgetKey + Attr = attr + AttributesBundle = AttributesBundle(StackList.empty(), ValueNone, ValueNone) } + + new(widgetKey: WidgetKey, attr: WidgetAttributeDefinition, attributesBundle: AttributesBundle) = + { WidgetKey = widgetKey + Attr = attr + AttributesBundle = attributesBundle } + + member inline this.Yield(widget: WidgetBuilder<'msg, 'childMarker>) = + SingleChildBuilderStep(fun () -> widget) + + member inline this.Combine + ( + [] a: SingleChildBuilderStep<'msg, 'childMarker>, + [] _b: SingleChildBuilderStep<'msg, 'childMarker> + ) = + SingleChildBuilderStep(fun () -> + // We only want one child, so we ignore the second one + a.Invoke()) + + member inline this.Delay([] fn: unit -> SingleChildBuilderStep<'msg, 'childMarker>) = + SingleChildBuilderStep(fun () -> fn().Invoke()) + + member inline this.Run([] result: SingleChildBuilderStep<'msg, 'childMarker>) = + let childAttr = this.Attr.WithValue(result.Invoke().Compile()) + let struct (scalars, widgets, widgetCollections) = this.AttributesBundle + + WidgetBuilder<'msg, 'marker>( + this.WidgetKey, + AttributesBundle( + scalars, + (match widgets with + | ValueNone -> ValueSome [| childAttr |] + | ValueSome widgets -> ValueSome(Array.appendOne childAttr widgets)), + widgetCollections + ) + ) diff --git a/src/Fabulous/Component/Component.fs b/src/Fabulous/Component.fs similarity index 74% rename from src/Fabulous/Component/Component.fs rename to src/Fabulous/Component.fs index a93a932c3..e6af90140 100644 --- a/src/Fabulous/Component/Component.fs +++ b/src/Fabulous/Component.fs @@ -1,8 +1,6 @@ namespace Fabulous open System -open System.Runtime.CompilerServices -open Fabulous.ScalarAttributeDefinitions (* @@ -198,13 +196,19 @@ let avatar1 = Component(sharedContext, Avatar()) let avatar2 = Component(sharedContext, Avatar()) avatar1.Background <- Blue -// Automatically triggers avator2.Background to become Blue +// Automatically triggers avatar2.Background to become Blue *) +/// This measure type is used to count the number of bindings in a component while building the computation expression +[] +type binding type ComponentBody = delegate of ComponentContext -> struct (ComponentContext * Widget) +[] +type ComponentData = { Body: ComponentBody } + type Component(treeContext: ViewTreeContext, body: ComponentBody, context: ComponentContext) = let mutable _body = body let mutable _context = context @@ -212,73 +216,67 @@ type Component(treeContext: ViewTreeContext, body: ComponentBody, context: Compo let mutable _view = null let mutable _contextSubscription: IDisposable = null - // TODO: This is a big code smell. We should not do this but I can't think of a better way to do it right now. - // The implementation of this method is set by the consuming project: Fabulous.XamarinForms, Fabulous.Maui, Fabulous.Avalonia - static let mutable _setAttachedComponent: obj -> Component -> unit = - fun _ _ -> failwith "Please call Component.SetComponentFunctions() before using Component" + interface IDisposable with + member this.Dispose() = + if _contextSubscription <> null then + _contextSubscription.Dispose() + _contextSubscription <- null - static let mutable _getAttachedComponent: obj -> Component = - fun _ -> failwith "Please call Component.SetComponentFunctions() before using Component" + member private this.MergeAttributes(rootWidget: Widget, componentWidgetOpt: Widget voption) = + match componentWidgetOpt with + | ValueNone -> struct (rootWidget.ScalarAttributes, rootWidget.WidgetAttributes, rootWidget.WidgetCollectionAttributes) - static member SetComponentFunctions(get: obj -> Component, set: obj -> Component -> unit) = - _getAttachedComponent <- get - _setAttachedComponent <- set + | ValueSome componentWidget -> + let componentScalars = + match componentWidget.ScalarAttributes with + | ValueNone -> ValueNone + | ValueSome attrs -> ValueSome(Array.skip 1 attrs) // skip the first attribute which is the component data - static member GetAttachedComponent(view: obj) = _getAttachedComponent view - static member SetAttachedComponent(view: obj, comp: Component) = _setAttachedComponent view comp + let scalars = + match struct (rootWidget.ScalarAttributes, componentScalars) with + | ValueNone, ValueNone -> ValueNone + | ValueSome attrs, ValueNone + | ValueNone, ValueSome attrs -> ValueSome attrs + | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append componentAttrs widgetAttrs) - member this.SetBody(body: ComponentBody) = - _body <- body - this.Render() + let widgets = + match struct (rootWidget.WidgetAttributes, componentWidget.WidgetAttributes) with + | ValueNone, ValueNone -> ValueNone + | ValueSome attrs, ValueNone + | ValueNone, ValueSome attrs -> ValueSome attrs + | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append componentAttrs widgetAttrs) - member this.SetContext(context: ComponentContext) = - _contextSubscription.Dispose() - _contextSubscription <- context.RenderNeeded.Subscribe(this.Render) - _context <- context - this.Render() + let widgetColls = + match struct (rootWidget.WidgetCollectionAttributes, componentWidget.WidgetCollectionAttributes) with + | ValueNone, ValueNone -> ValueNone + | ValueSome attrs, ValueNone + | ValueNone, ValueSome attrs -> ValueSome attrs + | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append componentAttrs widgetAttrs) - member this.CreateView(componentWidget: Widget) = + struct (scalars, widgets, widgetColls) + + member this.CreateView(componentWidget: Widget voption) = let struct (context, rootWidget) = _body.Invoke(_context) _widget <- rootWidget _context <- context - // Inject the attributes added to the component directly into the root widget - let scalars = - match componentWidget.ScalarAttributes with - | ValueNone -> ValueNone - | ValueSome attrs -> ValueSome(Array.skip 2 attrs) // Skip the Component_Body and Component_Context attributes + let struct (scalars, widgets, widgetColls) = + this.MergeAttributes(rootWidget, componentWidget) let rootWidget: Widget = { Key = rootWidget.Key #if DEBUG DebugName = rootWidget.DebugName #endif - ScalarAttributes = - match struct (rootWidget.ScalarAttributes, scalars) with - | ValueNone, ValueNone -> ValueNone - | ValueSome attrs, ValueNone - | ValueNone, ValueSome attrs -> ValueSome attrs - | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append widgetAttrs componentAttrs) - WidgetAttributes = - match struct (rootWidget.WidgetAttributes, componentWidget.WidgetAttributes) with - | ValueNone, ValueNone -> ValueNone - | ValueSome attrs, ValueNone - | ValueNone, ValueSome attrs -> ValueSome attrs - | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append widgetAttrs componentAttrs) - WidgetCollectionAttributes = - match struct (rootWidget.WidgetCollectionAttributes, componentWidget.WidgetCollectionAttributes) with - | ValueNone, ValueNone -> ValueNone - | ValueSome attrs, ValueNone - | ValueNone, ValueSome attrs -> ValueSome attrs - | ValueSome widgetAttrs, ValueSome componentAttrs -> ValueSome(Array.append widgetAttrs componentAttrs) } + ScalarAttributes = scalars + WidgetAttributes = widgets + WidgetCollectionAttributes = widgetColls } // Create the actual view let widgetDef = WidgetDefinitionStore.get rootWidget.Key let struct (node, view) = widgetDef.CreateView(rootWidget, treeContext, ValueNone) - _view <- view - - Component.SetAttachedComponent(view, this) + _view <- view _contextSubscription <- _context.RenderNeeded.Subscribe(this.Render) struct (node, view) @@ -298,31 +296,7 @@ type Component(treeContext: ViewTreeContext, body: ComponentBody, context: Compo Reconciler.update treeContext.CanReuseView (ValueSome prevRootWidget) currRootWidget viewNode - interface IDisposable with - member this.Dispose() = - if _contextSubscription <> null then - _contextSubscription.Dispose() - _contextSubscription <- null - module Component = - /// TODO: This is actually broken. On every call of the parent, the body will be reassigned to the Component triggering a re-render because of the noCompare. - /// This is not what was expected. The body should actually be invalidated based on its context. - let Body = - Attributes.defineSimpleScalar "Component_Body" ScalarAttributeComparers.noCompare (fun _ currOpt node -> - let target = Component.GetAttachedComponent(node.Target) - - match currOpt with - | ValueNone -> failwith "Component widget must have a body" - | ValueSome body -> target.SetBody(body)) - - let Context = - Attributes.defineSimpleScalar "Component_Context" ScalarAttributeComparers.equalityCompare (fun _ currOpt node -> - let target = Component.GetAttachedComponent(node.Target) - - match currOpt with - | ValueNone -> target.SetContext(ComponentContext()) - | ValueSome context -> target.SetContext(context)) - let WidgetKey = let key = WidgetDefinitionStore.getNextKey() @@ -336,27 +310,54 @@ module Component = match widget.ScalarAttributes with | ValueNone -> failwith "Component widget must have a body" | ValueSome attrs -> - let body = - match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Body.Key) attrs with - | Some attr -> attr.Value :?> ComponentBody + let data = + match Array.tryHead attrs with + | Some attr -> attr.Value :?> ComponentData | None -> failwith "Component widget must have a body" - let context = - match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Context.Key) attrs with - | Some attr -> attr.Value :?> ComponentContext - | None -> failwith "Component widget must have a context" + let ctx = ComponentContext() + let comp = new Component(treeContext, data.Body, ctx) + let struct (node, view) = comp.CreateView(ValueSome widget) - let comp = new Component(treeContext, body, context) - let struct (node, view) = comp.CreateView(widget) + // TODO: Attach component to view so component is not discarded by GC struct (node, view) } WidgetDefinitionStore.set key definition - key -[] -type ComponentModifiers = - [] - static member inline withContext(this: WidgetBuilder<'msg, 'marker>, context: ComponentContext) = - this.AddScalar(Component.Context.WithValue(context)) + let Data = + Attributes.defineSimpleScalar "Component_Data" ScalarAttributeComparers.noCompare (fun _ _ _ -> ()) + +/// Delegate used by the ComponentBuilder to compose a component body +/// It will be aggressively inlined by the compiler leaving no overhead, only a pure function that returns a WidgetBuilder +type ComponentBodyBuilder<'marker> = delegate of bindings: int * context: ComponentContext -> struct (int * WidgetBuilder) + +type ComponentBuilder() = + member inline this.Yield(widgetBuilder: WidgetBuilder) = + ComponentBodyBuilder<'marker>(fun bindings ctx -> struct (bindings, widgetBuilder)) + + member inline this.Combine([] a: ComponentBodyBuilder<'marker>, [] b: ComponentBodyBuilder<'marker>) = + ComponentBodyBuilder<'marker>(fun bindings ctx -> + let struct (bindingsA, _) = a.Invoke(bindings, ctx) // discard the previous widget in the chain but we still need to count the bindings + let struct (bindingsB, resultB) = b.Invoke(bindings, ctx) + + // Calculate the total number of bindings between A and B + let resultBindings = (bindingsA + bindingsB) - bindings + + struct (resultBindings, resultB)) + + member inline this.Delay([] fn: unit -> ComponentBodyBuilder<'marker>) = + ComponentBodyBuilder<'marker>(fun bindings ctx -> + let sub = fn() + sub.Invoke(bindings, ctx)) + + member inline this.Run([] body: ComponentBodyBuilder<'marker>) = + let compiledBody = + ComponentBody(fun ctx -> + let struct (_, result) = body.Invoke(0, ctx) + struct (ctx, result.Compile())) + + let data = { Body = compiledBody } + + WidgetBuilder(Component.WidgetKey, Component.Data.WithValue(data)) diff --git a/src/Fabulous/Component/Binding.fs b/src/Fabulous/Component/Binding.fs deleted file mode 100644 index c684e51b2..000000000 --- a/src/Fabulous/Component/Binding.fs +++ /dev/null @@ -1,71 +0,0 @@ -namespace Fabulous - -open System.Runtime.CompilerServices - -(* - -The idea of Binding is to listen to a State<'T> that is managed by another Context and be able to update it -while notifying the two Contexts involved (source and target) - -let child (count: BindingRequest) = - view { - let! boundCount = bind count - - Button($"Count is {boundCount.Value}", fun () -> boundCount.Set(boundCount.Value + 1)) - } - -let parent = - view { - let! count = state 0 - - VStack() { - Text($"Count is {count.Value}") - child (Binding.ofState count) - } - } - -*) - -type Binding<'T> = delegate of unit -> StateValue<'T> - -[] -type BindingValue<'T> = - val public Context: ComponentContext - val public SourceContext: ComponentContext - val public SourceKey: int - val public SourceCurrentValue: 'T - - new(ctx, sourceCtx, sourceKey, sourceCurrentValue) = - { Context = ctx - SourceContext = sourceCtx - SourceKey = sourceKey - SourceCurrentValue = sourceCurrentValue } - - member inline this.Current = this.SourceCurrentValue - - member inline this.Set(value: 'T) = - this.SourceContext.SetValue(this.SourceKey, value) - this.Context.NeedsRender() - -[] -type BindingExtensions = - [] - static member inline Bind - ( - _: ComponentBuilder, - [] request: Binding<'T>, - [] continuation: BindingValue<'T> -> ComponentBodyBuilder<'msg, 'marker> - ) = - // Despite its name, ComponentBinding actual value is not stored in this component, but in the source component - // So, we do not need to increment the number of bindings here - ComponentBodyBuilder(fun bindings ctx -> - let source = request.Invoke() - - source.Context.RenderNeeded.Add(fun () -> ctx.NeedsRender()) - - let state = BindingValue<'T>(ctx, source.Context, source.Key, source.Current) - (continuation state).Invoke(bindings, ctx)) - -[] -module BindingHelpers = - let inline ``$`` (source: StateValue<'T>) = Binding(fun () -> source) diff --git a/src/Fabulous/Component/Builder.fs b/src/Fabulous/Component/Builder.fs deleted file mode 100644 index c52382c20..000000000 --- a/src/Fabulous/Component/Builder.fs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Fabulous - -/// Delegate used by the ComponentBuilder to compose a component body -/// It will be aggressively inlined by the compiler leaving no overhead, only a pure function that returns a WidgetBuilder -type ComponentBodyBuilder<'msg, 'marker> = - delegate of bindings: int * context: ComponentContext -> struct (int * WidgetBuilder<'msg, 'marker>) - -type ComponentBuilder() = - member inline this.Yield(widgetBuilder: WidgetBuilder<'msg, 'marker>) = - ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> struct (bindings, widgetBuilder)) - - member inline this.Combine([] a: ComponentBodyBuilder<'msg, 'marker>, [] b: ComponentBodyBuilder<'msg, 'marker>) = - ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> - let struct (bindingsA, _) = a.Invoke(bindings, ctx) // discard the previous widget in the chain but we still need to count the bindings - let struct (bindingsB, resultB) = b.Invoke(bindings, ctx) - - // Calculate the total number of bindings between A and B - let resultBindings = (bindingsA + bindingsB) - bindings - - struct (resultBindings, resultB)) - - member inline this.Delay([] fn: unit -> ComponentBodyBuilder<'msg, 'marker>) = - ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> - let sub = fn() - sub.Invoke(bindings, ctx)) - - member inline this.Run([] body: ComponentBodyBuilder<'msg, 'marker>) = - let compiledBody = - ComponentBody(fun ctx -> - let struct (_, result) = body.Invoke(0, ctx) - struct (ctx, result.Compile())) - - WidgetBuilder<'msg, 'marker>(Component.WidgetKey, Component.Body.WithValue(compiledBody)) diff --git a/src/Fabulous/Component/State.fs b/src/Fabulous/Component/State.fs deleted file mode 100644 index f6357ee11..000000000 --- a/src/Fabulous/Component/State.fs +++ /dev/null @@ -1,91 +0,0 @@ -namespace Fabulous - -open System.ComponentModel -open System.Runtime.CompilerServices - -type State<'T> = delegate of unit -> 'T - -/// DESIGN: State<'T> is meant to be very short lived. -/// It is created on Bind (let!) and destroyed at the end of a single ViewBuilder CE execution. -/// Due to its nature, it is very likely it will be captured by a closure and allocated to the memory heap when it's not needed. -/// -/// e.g. -/// -/// Button("Increment", fun () -> state.Set(state.Current + 1)) -/// -/// will become -/// -/// class Closure { -/// public State state; // Storing a struct on a class will allocate it on the heap -/// -/// public void Invoke() { -/// state.Set(state.Current + 1); -/// } -/// } -/// -/// class Program { -/// public void View() -/// { -/// var state = new State(...); -/// -/// // This will allocate both the closure and the state on the heap -/// // which the GC will have to clean up later -/// var closure = new Closure(state = state); -/// -/// return Button("Increment", closure); -/// } -/// } -/// -/// -/// The Set method is therefore marked inlinable to avoid creating a closure capturing State<'T> -/// Instead the closure will only capture Context (already a reference type), Key (int) and Current (can be consider to be obj). -/// The compiler will rewrite the lambda as follow: -/// Button("Increment", fun () -> ctx.SetValue(key, current + 1)) -/// -/// State<'T> is no longer involved in the closure and will be kept on the stack. -/// -/// One constraint of inlining is to have all used fields public: Context, Key, Current -/// But we don't wish to expose the Context and Key fields to the user, so we mark them as EditorBrowsable.Never -[] -type StateValue<'T> = - [] - val public Context: ComponentContext - - [] - val public Key: int - - val public Current: 'T - - new(ctx, key, value) = - { Context = ctx - Key = key - Current = value } - - member inline this.Set(value: 'T) = this.Context.SetValue(this.Key, value) - -[] -type StateExtensions = - [] - static member inline Bind - ( - _: ComponentBuilder, - [] fn: State<'T>, - [] continuation: StateValue<'T> -> ComponentBodyBuilder<'msg, 'marker> - ) = - ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> - let key = int bindings - - let value = - match ctx.TryGetValue<'T>(key) with - | ValueSome value -> value - | ValueNone -> - let newValue = fn.Invoke() - ctx.SetValue(key, newValue) - newValue - - let state = StateValue(ctx, key, value) - (continuation state).Invoke((bindings + 1), ctx)) - -[] -module StateHelpers = - let inline State<'T> (value: 'T) = State<'T>(fun () -> value) diff --git a/src/Fabulous/Component/Context.fs b/src/Fabulous/ComponentContext.fs similarity index 82% rename from src/Fabulous/Component/Context.fs rename to src/Fabulous/ComponentContext.fs index 6e524dffa..9789baed9 100644 --- a/src/Fabulous/Component/Context.fs +++ b/src/Fabulous/ComponentContext.fs @@ -1,5 +1,7 @@ namespace Fabulous +open System.ComponentModel + (* ARCHITECTURE NOTES: @@ -13,18 +15,17 @@ Given each state is assigned to a specific index and that Components will most l we can leverage the inlining capabilities of the ComponentBuilder to create an array with the right size. *) -/// This measure type is used to count the number of bindings in a component while building the computation expression -[] -type binding - /// /// Holds the values for the various states of a component. /// -type ComponentContext() = - // We assume that most components will have few values, so initialize it with a small array - let mutable values = Array.zeroCreate 3 +type ComponentContext(initialSize: int) = + let mutable values = Array.zeroCreate initialSize let renderNeeded = Event() + + // We assume that most components will have few values, so initialize it with a small array + new() = ComponentContext(3) + member this.RenderNeeded = renderNeeded.Publish member this.NeedsRender() = renderNeeded.Trigger() @@ -48,8 +49,14 @@ type ComponentContext() = else ValueSome(unbox<'T> value) - member internal this.SetValueInternal(key: int, value: 'T) = values[key] <- box value + [] + member this.SetValueInternal(key: int, value: 'T) = values[key] <- box value member this.SetValue(key: int, value: 'T) = - values[key] <- box value + this.SetValueInternal(key, value) this.NeedsRender() + +[] +type Context private () = + class + end diff --git a/src/Fabulous/Fabulous.fsproj b/src/Fabulous/Fabulous.fsproj index 615134e71..73572af2d 100644 --- a/src/Fabulous/Fabulous.fsproj +++ b/src/Fabulous/Fabulous.fsproj @@ -32,21 +32,20 @@ - - + + - + + + + + - - - - -