Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mvu component #1052

Merged
merged 4 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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