diff --git a/src/Fabulous.Tests/APISketchTests/APISketchTests.fs b/src/Fabulous.Tests/APISketchTests/APISketchTests.fs index 0fdb95a0b..9190ba3f8 100644 --- a/src/Fabulous.Tests/APISketchTests/APISketchTests.fs +++ b/src/Fabulous.Tests/APISketchTests/APISketchTests.fs @@ -1,6 +1,7 @@ namespace Fabulous.Tests.APISketchTests open Fabulous.StackAllocatedCollections +open Fabulous.Tests.APISketchTests.Platform open NUnit.Framework open Platform @@ -847,3 +848,207 @@ module Attributes = instance.ProcessMessage(()) Assert.AreEqual(label.TextColor, "red") + +module ViewHelpers = + type ChildMsg = | ChildClick + + type ParentMsg = + | ParentClick + | ParentTap + | ChildMessage of ChildMsg + + type GrandParentMsg = + | GrandParentClick + | GrandParentTap + | ParentMessage of ParentMsg + + [] + let ``Widget converted with View.map dispatches the right message type`` () = + let mutable expectedMsg = Unchecked.defaultof + + let childView = + Button("Child button", ChildClick) + .automationId("childButton") + + let parentView model = + Stack() { + Button("Parent button", ParentClick) + .automationId("parentButton") + + (View.map ChildMessage childView).tap(ParentTap) + } + + let init () = true + + let update msg model = + Assert.AreEqual(expectedMsg, msg) + not model + + let program = + StatefulWidget.mkSimpleView init update parentView + + let instance = Run.Instance program + let tree = instance.Start() + + let childButton = find tree "childButton" + let parentButton = find tree "parentButton" + + expectedMsg <- ParentClick + parentButton.Press() + + expectedMsg <- ChildMessage ChildClick + childButton.Press() + + [] + let ``Adding property modifiers to widget converted with View.map is valid`` () = + let childView = + Button("Child button", ChildClick) + .automationId("childButton") + + let parentView model = + Stack() { + (View.map ChildMessage childView) + .textColor("blue") + } + + let init () = true + let update _msg model = not model + + let program = + StatefulWidget.mkSimpleView init update parentView + + let instance = Run.Instance program + let tree = instance.Start() + + let childButton = + find tree "childButton" :> IText + + Assert.AreEqual(childButton.TextColor, "blue") + + [] + let ``Adding event modifiers to widget converted with View.map still dispatches the right message type`` () = + let mutable expectedMsg = Unchecked.defaultof + + let childView = + Button("Child button", ChildClick) + .automationId("childButton") + + let parentView model = + Stack() { (View.map ChildMessage childView).tap(ParentTap) } + + let init () = true + + let update msg model = + Assert.AreEqual(expectedMsg, msg) + not model + + let program = + StatefulWidget.mkSimpleView init update parentView + + let instance = Run.Instance program + let tree = instance.Start() + + let childButton = find tree "childButton" + + expectedMsg <- ParentTap + childButton.Tap() + + [] + let ``Several layers of View.map produce the right result`` () = + let mutable expectedMsg = Unchecked.defaultof + + let childView = + Button("Child button", ChildClick) + .automationId("childButton") + + let parentView = + Stack() { + Button("Parent button", ParentClick) + .automationId("parentButton") + + (View.map ChildMessage childView).tap(ParentTap) + } + + let grandParentView model = + Stack() { + Button("Grand Parent button", GrandParentClick) + .automationId("grandParentButton") + + (View.map ParentMessage parentView) + .automationId("parentStack") + .tapContainer(GrandParentTap) + } + + + let init () = true + + let update msg model = + Assert.AreEqual(expectedMsg, msg) + not model + + let program = + StatefulWidget.mkSimpleView init update grandParentView + + let instance = Run.Instance program + let tree = instance.Start() + + let childButton = find tree "childButton" + let parentButton = find tree "parentButton" + + let grandParentButton = + find tree "grandParentButton" + + let parentStack = find tree "parentStack" + + expectedMsg <- ParentMessage(ChildMessage ChildClick) + childButton.Press() + + expectedMsg <- ParentMessage ParentTap + childButton.Tap() + + expectedMsg <- ParentMessage ParentClick + parentButton.Press() + + expectedMsg <- GrandParentClick + grandParentButton.Press() + + expectedMsg <- GrandParentTap + parentStack.Tap() + + [] + let ``Cascading View.map produce the right result`` () = + let mutable expectedMsg = Unchecked.defaultof + + let childView = + Button("Child button", ChildClick) + .automationId("childButton") + + let parentView = + (View.map ChildMessage childView).tap(ParentTap) + + let grandParentView model = + (View.map ParentMessage parentView) + .tap2(GrandParentTap) + + let init () = true + + let update msg model = + Assert.AreEqual(expectedMsg, msg) + not model + + let program = + StatefulWidget.mkSimpleView init update grandParentView + + let instance = Run.Instance program + let tree = instance.Start() + + let childButton = find tree "childButton" + + expectedMsg <- ParentMessage(ChildMessage ChildClick) + childButton.Press() + + expectedMsg <- ParentMessage ParentTap + childButton.Tap() + + expectedMsg <- GrandParentTap + childButton.Tap2() diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Attributes.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Attributes.fs index 36683c991..f72e0005b 100644 --- a/src/Fabulous.Tests/APISketchTests/TestUI.Attributes.fs +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Attributes.fs @@ -35,6 +35,84 @@ module TestUI_Attributes = { Key = key; Name = name } + let defineTappable name : ScalarAttributeDefinition = + let key = + ScalarAttributeDefinition.CreateAttributeData( + (fun x -> x), + ScalarAttributeComparers.noCompare, + (fun _ newValueOpt node -> + + let btn = node.Target :?> IButton + + match node.TryGetHandler(name) with + | ValueNone -> () + | ValueSome handlerId -> btn.RemoveTapListener handlerId + + match newValueOpt with + | ValueNone -> node.SetHandler(name, ValueNone) + + | ValueSome msg -> + let handler () = Dispatcher.dispatch node msg + + let handlerId = btn.AddTapListener handler + node.SetHandler(name, ValueSome handlerId)) + ) + |> AttributeDefinitionStore.registerScalar + + { Key = key; Name = name } + + let defineTappable2 name : ScalarAttributeDefinition = + let key = + ScalarAttributeDefinition.CreateAttributeData( + (fun x -> x), + ScalarAttributeComparers.noCompare, + (fun _ newValueOpt node -> + + let btn = node.Target :?> IButton + + match node.TryGetHandler(name) with + | ValueNone -> () + | ValueSome handlerId -> btn.RemoveTap2Listener handlerId + + match newValueOpt with + | ValueNone -> node.SetHandler(name, ValueNone) + + | ValueSome msg -> + let handler () = Dispatcher.dispatch node msg + + let handlerId = btn.AddTap2Listener handler + node.SetHandler(name, ValueSome handlerId)) + ) + |> AttributeDefinitionStore.registerScalar + + { Key = key; Name = name } + + let defineContainerTappable name : ScalarAttributeDefinition = + let key = + ScalarAttributeDefinition.CreateAttributeData( + (fun x -> x), + ScalarAttributeComparers.noCompare, + (fun _ newValueOpt node -> + + let btn = node.Target :?> IContainer + + match node.TryGetHandler(name) with + | ValueNone -> () + | ValueSome handlerId -> btn.RemoveTapListener handlerId + + match newValueOpt with + | ValueNone -> node.SetHandler(name, ValueNone) + + | ValueSome msg -> + let handler () = Dispatcher.dispatch node msg + + let handlerId = btn.AddTapListener handler + node.SetHandler(name, ValueSome handlerId)) + ) + |> AttributeDefinitionStore.registerScalar + + { Key = key; Name = name } + @@ -59,8 +137,12 @@ module TestUI_Attributes = "Container_Children" (fun target -> (target :?> IContainer).Children :> System.Collections.Generic.IList<_>) + let Tap = defineContainerTappable "Container_Tap" + module Button = let Pressed = definePressable "Button_Pressed" + let Tap = defineTappable "Button_Tap" + let Tap2 = defineTappable2 "Button_Tap2" module Automation = diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs index f91c45b6a..6e23caff1 100644 --- a/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs @@ -13,14 +13,22 @@ module Platform = abstract member Text: string with get, set abstract member TextColor: string with get, set + type ContainerHandler = unit -> unit + type IContainer = abstract member Children: List + abstract AddTapListener: ContainerHandler -> int + abstract RemoveTapListener: int -> unit type ButtonHandler = unit -> unit type IButton = abstract AddPressListener: ButtonHandler -> int abstract RemovePressListener: int -> unit + abstract AddTapListener: ButtonHandler -> int + abstract RemoveTapListener: int -> unit + abstract AddTap2Listener: ButtonHandler -> int + abstract RemoveTap2Listener: int -> unit type LabelChangeList = | TextSet of string @@ -55,18 +63,42 @@ module Platform = type TestStack() = inherit TestViewElement() + let mutable tapCounter: int = 1 + let tapHandlers = Dictionary() + + member _.Tap() = + for handler in Array.ofSeq(tapHandlers.Values) do + handler() interface IContainer with member val Children = List() + member this.AddTapListener(handler) = + tapHandlers.Add(tapCounter, handler) + tapCounter <- tapCounter + 1 + tapCounter - 1 + + member this.RemoveTapListener(id) = tapHandlers.Remove(id) |> ignore type TestButton() = inherit TestViewElement() - let mutable counter: int = 1 - let handlers = Dictionary() + let mutable pressCounter: int = 1 + let mutable tapCounter: int = 1 + let mutable tap2Counter: int = 1 + let pressHandlers = Dictionary() + let tapHandlers = Dictionary() + let tap2Handlers = Dictionary() member _.Press() = - for handler in Array.ofSeq(handlers.Values) do + for handler in Array.ofSeq(pressHandlers.Values) do + handler() + + member _.Tap() = + for handler in Array.ofSeq(tapHandlers.Values) do + handler() + + member _.Tap2() = + for handler in Array.ofSeq(tap2Handlers.Values) do handler() interface IText with @@ -75,11 +107,25 @@ module Platform = interface IButton with member this.AddPressListener(handler) = - handlers.Add(counter, handler) - counter <- counter + 1 - counter - 1 + pressHandlers.Add(pressCounter, handler) + pressCounter <- pressCounter + 1 + pressCounter - 1 + + member this.RemovePressListener(id) = pressHandlers.Remove(id) |> ignore + + member this.AddTapListener(handler) = + tapHandlers.Add(tapCounter, handler) + tapCounter <- tapCounter + 1 + tapCounter - 1 + + member this.RemoveTapListener(id) = tapHandlers.Remove(id) |> ignore + + member this.AddTap2Listener(handler) = + tap2Handlers.Add(tap2Counter, handler) + tap2Counter <- tap2Counter + 1 + tap2Counter - 1 - member this.RemovePressListener(id) = handlers.Remove(id) |> ignore + member this.RemoveTap2Listener(id) = tap2Handlers.Remove(id) |> ignore type TestNumericBag() = diff --git a/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs b/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs index 6073ed37b..eaa460a40 100644 --- a/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs +++ b/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs @@ -107,6 +107,33 @@ module TestUI_Widgets = ) = this.AddScalar(Attributes.Text.Record.WithValue(value)) + + [] + static member inline tap<'msg, 'marker when 'marker :> TestButtonMarker> + ( + this: WidgetBuilder<'msg, 'marker>, + value: 'msg + ) = + this.AddScalar(Attributes.Button.Tap.WithValue(value)) + + + [] + static member inline tap2<'msg, 'marker when 'marker :> TestButtonMarker> + ( + this: WidgetBuilder<'msg, 'marker>, + value: 'msg + ) = + this.AddScalar(Attributes.Button.Tap2.WithValue(value)) + + + [] + static member inline tapContainer<'msg, 'marker when 'marker :> TestStackMarker> + ( + this: WidgetBuilder<'msg, 'marker>, + value: 'msg + ) = + this.AddScalar(Attributes.Container.Tap.WithValue(value)) + ///---------------- ///---------------- diff --git a/src/Fabulous/View.fs b/src/Fabulous/View.fs index 551917225..d38cc50a8 100644 --- a/src/Fabulous/View.fs +++ b/src/Fabulous/View.fs @@ -30,16 +30,24 @@ module View = /// Map the widget's message type to the parent's message type to allow for view composition let inline map (fn: 'oldMsg -> 'newMsg) (x: WidgetBuilder<'oldMsg, 'marker>) : WidgetBuilder<'newMsg, 'marker> = let replaceWith (oldAttr: ScalarAttribute) = - let fnWithBoxing: obj -> obj = - unbox obj> oldAttr.Value - >> unbox - >> fn - >> box + let fnWithBoxing (msg: obj) = + let oldFn = unbox obj> oldAttr.Value + + if typeof<'newMsg>.IsAssignableFrom (msg.GetType()) then + box msg + else + oldFn msg |> unbox<'oldMsg> |> fn |> box { oldAttr with Value = fnWithBoxing } let defaultWith () = - MapMsg.MapMsg.WithValue(unbox<'oldMsg> >> fn >> box) + let mappedFn (msg: obj) = + if typeof<'newMsg>.IsAssignableFrom (msg.GetType()) then + box msg + else + unbox<'oldMsg> msg |> fn |> box + + MapMsg.MapMsg.WithValue(mappedFn) let builder = x.AddOrReplaceScalar(MapMsg.MapMsg.Key, replaceWith, defaultWith)