diff --git a/extensions/Fabulous.Avalonia.ItemsRepeater/ItemsRepeater.fs b/extensions/Fabulous.Avalonia.ItemsRepeater/ItemsRepeater.fs index 1f0492280..c538e5d62 100644 --- a/extensions/Fabulous.Avalonia.ItemsRepeater/ItemsRepeater.fs +++ b/extensions/Fabulous.Avalonia.ItemsRepeater/ItemsRepeater.fs @@ -17,17 +17,17 @@ module ItemsRepeater = "ItemsRepeater_ItemsSource" (fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems) (fun _ newValueOpt node -> - let dataGrid = node.Target :?> ItemsRepeater + let repeater = node.Target :?> ItemsRepeater match newValueOpt with | ValueNone -> - dataGrid.ClearValue(ItemsRepeater.ItemTemplateProperty) - dataGrid.ClearValue(ItemsRepeater.ItemsSourceProperty) + repeater.ClearValue(ItemsRepeater.ItemTemplateProperty) + repeater.ClearValue(ItemsRepeater.ItemsSourceProperty) | ValueSome value -> - dataGrid.SetValue(ItemsRepeater.ItemTemplateProperty, WidgetDataTemplate(node, unbox >> value.Template)) + repeater.SetValue(ItemsRepeater.ItemTemplateProperty, WidgetDataTemplate(node, unbox >> value.Template)) |> ignore - dataGrid.SetValue(ItemsRepeater.ItemsSourceProperty, value.OriginalItems)) + repeater.SetValue(ItemsRepeater.ItemsSourceProperty, value.OriginalItems)) let HorizontalCacheLength = Attributes.defineAvaloniaPropertyWithEquality ItemsRepeater.HorizontalCacheLengthProperty diff --git a/samples/Gallery/Pages/AutoCompleteBoxPage.fs b/samples/Gallery/Pages/AutoCompleteBoxPage.fs index 6f2426ab9..9e8a14252 100644 --- a/samples/Gallery/Pages/AutoCompleteBoxPage.fs +++ b/samples/Gallery/Pages/AutoCompleteBoxPage.fs @@ -4,8 +4,10 @@ open System open System.Diagnostics open System.Threading open System.Threading.Tasks +open Avalonia.Animation open Avalonia.Controls open Avalonia.Interactivity +open Avalonia.Media open Fabulous open Fabulous.Avalonia @@ -209,29 +211,25 @@ module AutoCompleteBoxPage = Abbreviation = "WY" Capital = "Cheyenne" } ] + let contains (text: string) (term: string) = + text.Contains(term, StringComparison.InvariantCultureIgnoreCase) + /// allows searching US federal states asynchronously while cancelling running searches /// to ensure we only populate with the response of the latest request - type UsFederalStateSearch() = + type UsFederalStateSearch(animateActiveInput) = let mutable running: CancellationTokenSource = null // enables cancelling any running search + let isRunning () = running <> null + /// cancels any running search - let cancelRunning () = - if running <> null then + let cancel () = + if isRunning() then running.Cancel() running.Dispose() + running <- null - member this.SearchAsync (term: string) (cancellation: CancellationToken) : Task = + let simulateWork () = task { - // register cancellation of running search when outer cancellation is requested - cancellation.Register(fun () -> - cancelRunning() - running <- null) - |> ignore - - cancelRunning() // cancel any older running search - running <- new CancellationTokenSource() // and create a new source for this one - - // simulate a really sporadic remote search let randy = Random() let getDelay () = randy.Next(300, 3000) let mutable delay = getDelay() @@ -242,12 +240,21 @@ module AutoCompleteBoxPage = delay <- getDelay() do! Task.Delay(delay) // guarantee to wait a little bit + } + + member this.SearchAsync (term: string) (cancellation: CancellationToken) : Task = + task { + cancellation.Register(cancel) |> ignore // register cancellation of running search when outer cancellation is requested + cancel() // cancel any older running search + running <- new CancellationTokenSource() // and create a new source for this one + animateActiveInput(running.Token) // pass running search token to stop it when the search completes or is canceled + do! simulateWork() // simulate a really sporadic remote search - if running.IsCancellationRequested then + if isRunning() |> not || running.IsCancellationRequested then + cancel() // to stop animation return Seq.empty else - let contains (text: string) (term: string) = - text.Contains(term, StringComparison.InvariantCultureIgnoreCase) + cancel() // to stop animation return usFederalStates @@ -255,6 +262,16 @@ module AutoCompleteBoxPage = |> Seq.cast } + /// helps animating an active remote search AutoCompleteBox + module RemoteSearch = + let input = ViewRef() + let heartBeat = ViewRef() + + /// animates the input with the heartBeat until searchToken is cancelled + let animate searchToken = + heartBeat.Value.IterationCount <- IterationCount.Infinite + heartBeat.Value.RunAsync(input.Value, searchToken) |> ignore + type Model = { IsOpen: bool SelectedItem: string @@ -276,7 +293,7 @@ module AutoCompleteBoxPage = { IsOpen = false Text = "Arkan" AsyncSearchTerm = "" - UsStateSearch = UsFederalStateSearch() + UsStateSearch = UsFederalStateSearch(RemoteSearch.animate) SelectedItem = "Item 2" Items = [ "Item 1"; "Item 2"; "Item 3"; "Product 1"; "Product 2"; "Product 3" ] UsFederalStates = usFederalStates @@ -437,6 +454,24 @@ module AutoCompleteBoxPage = .multiBindValue("{0} ({1})", nameof stateData.Name, nameof stateData.Abbreviation) } + VStack() { + TextBlock("With an item template") + .tip(ToolTip("Somewhere, in pride, an eagle sheds\nA single splendid tear.")) + + AutoCompleteBox(model.UsFederalStates) + .itemTemplate(fun state -> + HStack(5) { + TextBlock(state.Capital).foreground(Colors.Blue) + TextBlock(state.Abbreviation + ",").foreground(Colors.White) + TextBlock(state.Name).foreground(Colors.Red) + }) + .watermark("Search a US state or capital") + .tip(ToolTip("the custom item filter searches the state name as well as the capital")) + .itemFilter(fun term item -> + let state = item :?> StateData + contains state.Name term || contains state.Capital term) + } + VStack() { TextBlock("AsyncBox") @@ -454,6 +489,23 @@ module AutoCompleteBoxPage = .onTextChanged(model.AsyncSearchTerm, AsyncSearchTermChanged) .filterMode(AutoCompleteFilterMode.None) // remote filtered .multiBindValue("{2}, {1} ({0})", nameof stateData.Name, nameof stateData.Abbreviation, nameof stateData.Capital) + .reference(RemoteSearch.input) + .animation( + // pulses the scale like a heart beat to indicate activity + (Animation(TimeSpan.FromSeconds(2.)) { + // extend slightly but quickly to get a pulse effect + KeyFrame(ScaleTransform.ScaleXProperty, 1.05).cue(0.1) + KeyFrame(ScaleTransform.ScaleYProperty, 1.05).cue(0.1) + // contract slightly to get a bounce-back effect + KeyFrame(ScaleTransform.ScaleXProperty, 0.95).cue(0.15) + KeyFrame(ScaleTransform.ScaleYProperty, 0.95).cue(0.15) + // return to original size rather quickly + KeyFrame(ScaleTransform.ScaleXProperty, 1).cue(0.2) + KeyFrame(ScaleTransform.ScaleYProperty, 1).cue(0.2) + }) + .delay(TimeSpan.FromSeconds 1.) // to avoid a "heart attack", i.e. restarting the animation by typing + .reference(RemoteSearch.heartBeat) + ) } VStack() { diff --git a/samples/Gallery/Pages/GesturesPage.fs b/samples/Gallery/Pages/GesturesPage.fs index f3c5ed48e..03c3d78b3 100644 --- a/samples/Gallery/Pages/GesturesPage.fs +++ b/samples/Gallery/Pages/GesturesPage.fs @@ -17,8 +17,6 @@ module GesturesPage = let init () = { CurrentScale = 0 }, Cmd.none - let topBallBorderRef = ViewRef() - let update msg model = match msg with | Reset -> model, Cmd.none @@ -61,7 +59,6 @@ module GesturesPage = .dock(Dock.Top) .margin(2.) .name("TopPullZone") - .reference(topBallBorderRef) .background(SolidColorBrush(Colors.Transparent)) .borderBrush(SolidColorBrush(Colors.Red)) .horizontalAlignment(HorizontalAlignment.Stretch) diff --git a/samples/Gallery/Pages/Pointers/PointersPage.fs b/samples/Gallery/Pages/Pointers/PointersPage.fs index 535475db3..979b467c1 100644 --- a/samples/Gallery/Pages/Pointers/PointersPage.fs +++ b/samples/Gallery/Pages/Pointers/PointersPage.fs @@ -25,18 +25,12 @@ module PointersPage = | Border_PointerCaptureLost of PointerCaptureLostEventArgs | Border_PointerUpdated of PointerEventArgs - let pointerCanvasRef = ViewRef() - let init () = { ThreadSleep = 0 Status = "" Status2 = "" }, Cmd.none - let border1 = ViewRef() - - let border2 = ViewRef() - let update msg model = match msg with | ThreadSleepSliderChanged v -> { model with ThreadSleep = int v }, Cmd.none diff --git a/samples/Gallery/Pages/PopupPage.fs b/samples/Gallery/Pages/PopupPage.fs index f91fecc42..167dc0dc1 100644 --- a/samples/Gallery/Pages/PopupPage.fs +++ b/samples/Gallery/Pages/PopupPage.fs @@ -26,8 +26,6 @@ module PopupPage = | OnOpened -> model, Cmd.none | OnClosed -> model, Cmd.none - let buttonRef = ViewRef