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

Adding AutoCompleteBox extension for setting ItemTemplate #244

Merged
merged 11 commits into from
May 20, 2024
10 changes: 5 additions & 5 deletions extensions/Fabulous.Avalonia.ItemsRepeater/ItemsRepeater.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 70 additions & 18 deletions samples/Gallery/Pages/AutoCompleteBoxPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<obj seq> =
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()
Expand All @@ -242,19 +240,38 @@ module AutoCompleteBoxPage =
delay <- getDelay()

do! Task.Delay(delay) // guarantee to wait a little bit
}

member this.SearchAsync (term: string) (cancellation: CancellationToken) : Task<obj seq> =
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
|> Seq.filter(fun state -> contains state.Name term || contains state.Capital term)
|> Seq.cast<obj>
}

/// helps animating an active remote search AutoCompleteBox
module RemoteSearch =
let input = ViewRef<AutoCompleteBox>()
let heartBeat = ViewRef<Animation>()

/// 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
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand All @@ -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() {
Expand Down
3 changes: 0 additions & 3 deletions samples/Gallery/Pages/GesturesPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ module GesturesPage =

let init () = { CurrentScale = 0 }, Cmd.none

let topBallBorderRef = ViewRef<Border>()

let update msg model =
match msg with
| Reset -> model, Cmd.none
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 0 additions & 6 deletions samples/Gallery/Pages/Pointers/PointersPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,12 @@ module PointersPage =
| Border_PointerCaptureLost of PointerCaptureLostEventArgs
| Border_PointerUpdated of PointerEventArgs

let pointerCanvasRef = ViewRef<PointerCanvas>()

let init () =
{ ThreadSleep = 0
Status = ""
Status2 = "" },
Cmd.none

let border1 = ViewRef<Border>()

let border2 = ViewRef<Border>()

let update msg model =
match msg with
| ThreadSleepSliderChanged v -> { model with ThreadSleep = int v }, Cmd.none
Expand Down
2 changes: 0 additions & 2 deletions samples/Gallery/Pages/PopupPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ module PopupPage =
| OnOpened -> model, Cmd.none
| OnClosed -> model, Cmd.none

let buttonRef = ViewRef<Button>()

let program =
Program.statefulWithCmd init update
|> Program.withTrace(fun (format, args) -> Debug.WriteLine(format, box args))
Expand Down
11 changes: 6 additions & 5 deletions samples/Gallery/Pages/TreeDataGrid/CountriesPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -393,25 +393,26 @@ module CountriesPage =
Label("_Country").target(countryTextBox)

TextBox(model.CountryText, CountryTextChanged)
.name("countryTextBox")
.reference(countryTextBox)

Label("_Region").target(regionTextBox)

TextBox(model.RegionText, RegionTextChanged)
.name("regionTextBox")
.reference(regionTextBox)

Label("_Population").target(populationTextBox)

TextBox(model.PopulationText, PopulationTextChanged)
.name("populationTextBox")
.reference(populationTextBox)

Label("_Area").target(areaTextBox)

TextBox(model.AreaText, AreaTextChanged).name("areaTextBox")
TextBox(model.AreaText, AreaTextChanged)
.reference(areaTextBox)

Label("_GDP").target(gdpTextBox)

TextBox(model.GDPText, GDPTextChanged).name("gdpTextBox")
TextBox(model.GDPText, GDPTextChanged).reference(gdpTextBox)

Button("Add", AddCountryClick)

Expand Down
6 changes: 1 addition & 5 deletions samples/Gallery/Pages/TreeDataGrid/FilesPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ module FilesPage =
| SelectedPathKeyDown of Avalonia.Input.KeyEventArgs
| CellSelectionChanged of bool

let files = ViewRef<TreeDataGrid>()

let init () =
let drives = DriveInfo.GetDrives() |> Array.map(fun x -> x.Name) |> Array.toList

Expand Down Expand Up @@ -220,8 +218,6 @@ module FilesPage =
.dock(Dock.Top)
.margin(0, 4)

TreeDataGrid(model.Source)
.reference(files)
.autoDragDropRows(true)
TreeDataGrid(model.Source).autoDragDropRows(true)
}
}
8 changes: 8 additions & 0 deletions src/Fabulous.Avalonia/Attributes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ module ValueEventData =
{ Value = value
Event = event >> box >> MsgValue }

[<RequireQualifiedAccess>]
module ScalarAttributeComparers =
let inline physicalEqualityCompare a b =
if LanguagePrimitives.PhysicalEquality a b then
ScalarAttributeComparison.Identical
else
ScalarAttributeComparison.Different

module Attributes =
/// Define an attribute for EventHandler<'T>
let inline defineAvaloniaObservableEvent<'args>
Expand Down
18 changes: 18 additions & 0 deletions src/Fabulous.Avalonia/Views/Controls/AutoCompleteBox.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ module AutoCompleteBox =

target.Loaded.AddHandler(bindAndCleanUp))

/// Allows setting the ItemTemplate on an AutoCompleteBox
let ItemTemplate =
Attributes.defineSimpleScalar<obj -> Widget> "AutoCompleteBox_ItemTemplate" ScalarAttributeComparers.physicalEqualityCompare (fun _ newValueOpt node ->
let autoComplete = node.Target :?> AutoCompleteBox

match newValueOpt with
| ValueNone -> autoComplete.ClearValue(AutoCompleteBox.ItemTemplateProperty)
| ValueSome template ->
autoComplete.SetValue(AutoCompleteBox.ItemTemplateProperty, WidgetDataTemplate(node, template))
|> ignore)

[<AutoOpen>]
module AutoCompleteBoxBuilders =
type Fabulous.Avalonia.View with
Expand Down Expand Up @@ -144,6 +155,13 @@ type AutoCompleteBoxModifiers =
static member inline itemFilter(this: WidgetBuilder<'msg, #IFabAutoCompleteBox>, fn: string -> obj -> bool) =
this.AddScalar(AutoCompleteBox.ItemFilter.WithValue(fn))

/// <summary>Sets the ItemTemplate property.</summary>
edgarfgp marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="this">Current widget.</param>
/// <param name="template">The template to render the items with.</param>
[<Extension>]
static member inline itemTemplate(this: WidgetBuilder<'msg, #IFabAutoCompleteBox>, template: 'item -> WidgetBuilder<'msg, #IFabControl>) =
this.AddScalar(AutoCompleteBox.ItemTemplate.WithValue(WidgetHelpers.compileTemplate template))

/// <summary>Sets the TextFilter property.</summary>
/// <param name="this">Current widget.</param>
/// <param name="value">The TextFilter value.</param>
Expand Down
10 changes: 5 additions & 5 deletions src/Fabulous.Avalonia/Views/ItemsControl.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ module ItemsControl =
"ItemsControl_ItemsSource"
(fun a b -> ScalarAttributeComparers.equalityCompare a.OriginalItems b.OriginalItems)
(fun _ newValueOpt node ->
let listBox = node.Target :?> ItemsControl
let itmsCtrl = node.Target :?> ItemsControl

match newValueOpt with
| ValueNone ->
listBox.ClearValue(ItemsControl.ItemTemplateProperty)
listBox.ClearValue(ItemsControl.ItemsSourceProperty)
itmsCtrl.ClearValue(ItemsControl.ItemTemplateProperty)
itmsCtrl.ClearValue(ItemsControl.ItemsSourceProperty)
| ValueSome value ->
listBox.SetValue(ItemsControl.ItemTemplateProperty, WidgetDataTemplate(node, unbox >> value.Template))
itmsCtrl.SetValue(ItemsControl.ItemTemplateProperty, WidgetDataTemplate(node, unbox >> value.Template))
|> ignore

listBox.SetValue(ItemsControl.ItemsSourceProperty, value.OriginalItems)
itmsCtrl.SetValue(ItemsControl.ItemsSourceProperty, value.OriginalItems)
|> ignore)

let ItemsPanel =
Expand Down
11 changes: 6 additions & 5 deletions src/Fabulous.Avalonia/Widgets.fs
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,21 @@ module Widgets =
let register<'T when WidgetOps<'T>> () = registerWithFactory(fun () -> new 'T())

module WidgetHelpers =
/// Compiles the templateBuilder into a template.
let compileTemplate (templateBuilder: 'item -> WidgetBuilder<'msg, 'widget>) item =
let itm = unbox<'item> item
(templateBuilder itm).Compile()

/// Creates a widget with the given key and attributes.
let buildItems<'msg, 'marker, 'itemData, 'itemMarker>
key
(attrDef: SimpleScalarAttributeDefinition<WidgetItems>)
(items: seq<'itemData>)
(itemTemplate: 'itemData -> WidgetBuilder<'msg, 'itemMarker>)
=
let template (item: obj) =
let item = unbox<'itemData> item
(itemTemplate item).Compile()

let data: WidgetItems =
{ OriginalItems = items
Template = template }
Template = compileTemplate itemTemplate }

WidgetBuilder<'msg, 'marker>(key, attrDef.WithValue(data))

Expand Down