fabulous-dev / Fabulous

Declarative UI framework for cross-platform mobile & desktop apps, using MVU and F# functional programming
https://fabulous.dev
Apache License 2.0
1.13k stars 122 forks source link

Add helper to return different controls in a same conditional expression #925

Closed TimLariviere closed 2 years ago

TimLariviere commented 2 years ago

To ensure type-safety when composing views, we have marker interfaces that simulate inheritance. This enables us to enforce that a parent type (eg. VStack) will only receive compatible children (inheriting from View in this instance).

VStack() {
    Button() // OK because Button == IButton (inherits IView)
    Label() // OK because Label == ILabel (inherits IView)

    TextCell() // NOT OK - TextCell == ICell but ICell doesn't inherit IView
}

This is great, but there is a catch.

Since we have to declare views using structs to avoid killing the performance by instantiating a lot, we can't do inheritance the normal way (structs can't inherit structs in .NET), so the marker interface is used as a generic type like this: Button() will return a WidgetBuilder<'msg, IButton>.

F# doesn't support covariance/contravariance... This means we can't cast a WidgetBuilder<'msg, IButton> into a WidgetBuilder<'msg, IView>, even though IButton inherits from IView.

This is problematic when we want to return different controls in a same conditional expression.

if model.Value then
    Button()
else
    Label() /// Compile Error - WidgetBuilder<'msg, ILabel> is not of type WidgetBuilder<'msg, IButton>

The return type of a conditional expression is determined by the first branch -- here the Button, so the F# compiler expects all other branches to return a button as well.

To work around this issue, I'm introducing a concept stolen directly from SwiftUI: AnyView (and the equivalent AnyPage and AnyCell).

This is a helper you can wrap around each conditional branch and it will downcast everything into a same type. You can even continue adding modifiers to the returned widget.


// Return type for the whole expression is WidgetBuilder<'msg, IView>
let widget =
    if model.Value then
        AnyView(
            Button()
        )
    else
        AnyView(
           Label()
        )

// Since this is IView, we have access to all IView modifiers
widget.backgroundColor(Color.Red)