Skip to content

Commit

Permalink
Merge pull request #1052 from fabulous-dev/mvu-component
Browse files Browse the repository at this point in the history
Mvu component
  • Loading branch information
TimLariviere committed Nov 25, 2023
2 parents 259b593 + dd2e4d0 commit c573ca8
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 98 deletions.
49 changes: 49 additions & 0 deletions src/Fabulous/Builders.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Fabulous

open System.ComponentModel
open Fabulous.WidgetAttributeDefinitions
open Fabulous.WidgetCollectionAttributeDefinitions
open Fabulous.StackAllocatedCollections
open Fabulous.StackAllocatedCollections.StackList
Expand Down Expand Up @@ -217,3 +218,51 @@ type AttributeCollectionBuilder<'msg, 'marker, 'itemMarker> =

res
end

type SingleChildBuilderStep<'msg, 'marker> = delegate of unit -> WidgetBuilder<'msg, 'marker>

[<Struct>]
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
(
[<InlineIfLambda>] a: SingleChildBuilderStep<'msg, 'childMarker>,
[<InlineIfLambda>] _b: SingleChildBuilderStep<'msg, 'childMarker>
) =
SingleChildBuilderStep(fun () ->
// We only want one child, so we ignore the second one
a.Invoke())

member inline this.Delay([<InlineIfLambda>] fn: unit -> SingleChildBuilderStep<'msg, 'childMarker>) =
SingleChildBuilderStep(fun () -> fn().Invoke())

member inline this.Run([<InlineIfLambda>] 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
)
)
2 changes: 1 addition & 1 deletion src/Fabulous/Component/Binding.fs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type BindingExtensions =
(
_: ComponentBuilder,
[<InlineIfLambda>] request: Binding<'T>,
[<InlineIfLambda>] continuation: BindingValue<'T> -> ComponentBodyBuilder<'msg, 'marker>
[<InlineIfLambda>] continuation: BindingValue<'T> -> ComponentBodyBuilder<'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
Expand Down
19 changes: 9 additions & 10 deletions src/Fabulous/Component/Builder.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ 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<binding> * context: ComponentContext -> struct (int<binding> * WidgetBuilder<'msg, 'marker>)
type ComponentBodyBuilder<'marker> = delegate of bindings: int<binding> * context: ComponentContext -> struct (int<binding> * WidgetBuilder<unit, 'marker>)

type ComponentBuilder() =
member inline this.Yield(widgetBuilder: WidgetBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> struct (bindings, widgetBuilder))
member inline this.Yield(widgetBuilder: WidgetBuilder<unit, 'marker>) =
ComponentBodyBuilder<'marker>(fun bindings ctx -> struct (bindings, widgetBuilder))

member inline this.Combine([<InlineIfLambda>] a: ComponentBodyBuilder<'msg, 'marker>, [<InlineIfLambda>] b: ComponentBodyBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx ->
member inline this.Combine([<InlineIfLambda>] a: ComponentBodyBuilder<'marker>, [<InlineIfLambda>] 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)

Expand All @@ -19,15 +18,15 @@ type ComponentBuilder() =

struct (resultBindings, resultB))

member inline this.Delay([<InlineIfLambda>] fn: unit -> ComponentBodyBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx ->
member inline this.Delay([<InlineIfLambda>] fn: unit -> ComponentBodyBuilder<'marker>) =
ComponentBodyBuilder<'marker>(fun bindings ctx ->
let sub = fn()
sub.Invoke(bindings, ctx))

member inline this.Run([<InlineIfLambda>] body: ComponentBodyBuilder<'msg, 'marker>) =
member inline this.Run([<InlineIfLambda>] body: ComponentBodyBuilder<'marker>) =
let compiledBody =
ComponentBody(fun ctx ->
let struct (_, result) = body.Invoke(0<binding>, ctx)
struct (ctx, result.Compile()))

WidgetBuilder<'msg, 'marker>(Component.WidgetKey, Component.Body.WithValue(compiledBody))
WidgetBuilder<unit, 'marker>(ComponentWidget.WidgetKey, Component.Body.WithValue(compiledBody))
111 changes: 59 additions & 52 deletions src/Fabulous/Component/Component.fs
Original file line number Diff line number Diff line change
Expand Up @@ -205,37 +205,67 @@ avatar1.Background <- Blue

type ComponentBody = delegate of ComponentContext -> struct (ComponentContext * Widget)

type Component(treeContext: ViewTreeContext, body: ComponentBody, context: ComponentContext) =
let mutable _body = body
let mutable _context = context
let mutable _widget = Unchecked.defaultof<_>
let mutable _view = null
let mutable _contextSubscription: IDisposable = null
type IBaseComponent =
inherit IDisposable

type IComponent =
inherit IBaseComponent
abstract member SetBody: ComponentBody -> unit
abstract member SetContext: ComponentContext -> unit

module Component =
// 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 =
let mutable setAttachedComponent: obj -> IBaseComponent -> unit =
fun _ _ -> failwith "Please call Component.SetComponentFunctions() before using Component"

static let mutable _getAttachedComponent: obj -> Component =
let mutable getAttachedComponent: obj -> IBaseComponent =
fun _ -> failwith "Please call Component.SetComponentFunctions() before using Component"

static member SetComponentFunctions(get: obj -> Component, set: obj -> Component -> unit) =
_getAttachedComponent <- get
_setAttachedComponent <- set
let setComponentFunctions (get: obj -> IBaseComponent, set: obj -> IBaseComponent -> unit) =
getAttachedComponent <- get
setAttachedComponent <- set

static member GetAttachedComponent(view: obj) = _getAttachedComponent view
static member SetAttachedComponent(view: obj, comp: Component) = _setAttachedComponent view comp
/// 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 = getAttachedComponent(node.Target) :?> IComponent

member this.SetBody(body: ComponentBody) =
_body <- body
this.Render()
match currOpt with
| ValueNone -> failwith "Component widget must have a body"
| ValueSome body -> target.SetBody(body))

member this.SetContext(context: ComponentContext) =
_contextSubscription.Dispose()
_contextSubscription <- context.RenderNeeded.Subscribe(this.Render)
_context <- context
this.Render()
let Context =
Attributes.defineSimpleScalar "Component_Context" ScalarAttributeComparers.equalityCompare (fun _ currOpt node ->
let target = getAttachedComponent(node.Target) :?> IComponent

match currOpt with
| ValueNone -> target.SetContext(ComponentContext())
| ValueSome context -> target.SetContext(context))

type Component(treeContext: ViewTreeContext, body: ComponentBody, context: ComponentContext) =
let mutable _body = body
let mutable _context = context
let mutable _widget = Unchecked.defaultof<_>
let mutable _view = null
let mutable _contextSubscription: IDisposable = null

interface IComponent with
member this.SetBody(body: ComponentBody) =
_body <- body
this.Render()

member this.SetContext(context: ComponentContext) =
_contextSubscription.Dispose()
_contextSubscription <- context.RenderNeeded.Subscribe(this.Render)
_context <- context
this.Render()

member this.Dispose() =
if _contextSubscription <> null then
_contextSubscription.Dispose()
_contextSubscription <- null

member this.CreateView(componentWidget: Widget) =
let struct (context, rootWidget) = _body.Invoke(_context)
Expand All @@ -246,7 +276,8 @@ type Component(treeContext: ViewTreeContext, body: ComponentBody, context: Compo
let scalars =
match componentWidget.ScalarAttributes with
| ValueNone -> ValueNone
| ValueSome attrs -> ValueSome(Array.skip 2 attrs) // Skip the Component_Body and Component_Context attributes
| ValueSome attrs ->
ValueSome(Array.filter (fun (attr: ScalarAttribute) -> attr.Key <> Component.Body.Key && attr.Key <> Component.Context.Key) attrs)

let rootWidget: Widget =
{ Key = rootWidget.Key
Expand Down Expand Up @@ -277,7 +308,7 @@ type Component(treeContext: ViewTreeContext, body: ComponentBody, context: Compo
let struct (node, view) = widgetDef.CreateView(rootWidget, treeContext, ValueNone)
_view <- view

Component.SetAttachedComponent(view, this)
Component.setAttachedComponent view this

_contextSubscription <- _context.RenderNeeded.Subscribe(this.Render)

Expand All @@ -298,31 +329,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))

module ComponentWidget =
let WidgetKey =
let key = WidgetDefinitionStore.getNextKey()

Expand All @@ -334,17 +341,17 @@ module Component =
CreateView =
fun (widget, treeContext, _) ->
match widget.ScalarAttributes with
| ValueNone -> failwith "Component widget must have a body"
| ValueNone -> failwith "Component widget must have a body and a context"
| ValueSome attrs ->
let body =
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Body.Key) attrs with
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Component.Body.Key) attrs with
| Some attr -> attr.Value :?> ComponentBody
| None -> failwith "Component widget must have a body"

let context =
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Context.Key) attrs with
match Array.tryFind (fun (attr: ScalarAttribute) -> attr.Key = Component.Context.Key) attrs with
| Some attr -> attr.Value :?> ComponentContext
| None -> failwith "Component widget must have a context"
| None -> ComponentContext()

let comp = new Component(treeContext, body, context)
let struct (node, view) = comp.CreateView(widget)
Expand Down
14 changes: 10 additions & 4 deletions src/Fabulous/Component/Context.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Fabulous

open System.ComponentModel

(*
ARCHITECTURE NOTES:
Expand All @@ -20,11 +22,14 @@ type binding
/// <summary>
/// Holds the values for the various states of a component.
/// </summary>
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<unit>()

// 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()

Expand All @@ -48,7 +53,8 @@ type ComponentContext() =
else
ValueSome(unbox<'T> value)

member internal this.SetValueInternal(key: int, value: 'T) = values[key] <- box value
[<EditorBrowsable(EditorBrowsableState.Never)>]
member this.SetValueInternal(key: int, value: 'T) = values[key] <- box value

member this.SetValue(key: int, value: 'T) =
values[key] <- box value
Expand Down
Loading

0 comments on commit c573ca8

Please sign in to comment.