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 @@
-
-
+
+
-
+
+
+
+
+
-
-
-
-
-