@@ -76,12 +76,13 @@ public sealed class FwPosChooser : Border
7676 private readonly bool _allowEmpty ;
7777 private string _emptyLabelText ;
7878
79- // Collapsed presentation (the field-sized box the user sees when the popup is closed).
79+ // Collapsed presentation (the field-sized box the user sees when the dropdown is closed).
8080 private readonly ToggleButton _dropdownButton ;
8181 private readonly TextBlock _dropdownLabel ;
82- private readonly Popup _popup ;
82+ private readonly Flyout _flyout ;
83+ private bool _open ;
8384
84- // Popup content: a filter box stacked over EITHER the tree (no filter) or a flat result list (filter).
85+ // Dropdown content: a filter box stacked over EITHER the tree (no filter) or a flat result list (filter).
8586 private readonly TextBox _filterBox ;
8687 private readonly TreeView _tree ;
8788 private readonly ListBox _filterList ;
@@ -238,32 +239,29 @@ public FwPosChooser(string automationId, bool allowEmpty = true, string emptyLab
238239 MinWidth = FwAvaloniaDensity . DropdownMinWidth + 20 ,
239240 Child = body
240241 } ;
241- // The popup renders in its OWN top-level (PopupRoot), so keys typed in the filter box do not
242- // bubble to the chooser root. Register the navigation handler on the popup panel so
243- // Up/Down/Enter/Escape work while it is open (same pattern as FwOptionPicker dropdown mode).
242+ AutomationProperties . SetAutomationId ( _popupPanel , _automationId + ".Popup" ) ;
243+ // The flyout body renders in its OWN top-level, so keys typed in the filter box do not bubble
244+ // to the chooser root. Register the navigation handler on the panel so Up/Down/Enter/Escape
245+ // work while it is open (same pattern as FwOptionPicker dropdown mode).
244246 _popupPanel . AddHandler ( InputElement . KeyDownEvent , OnPopupKeyDown ,
245247 RoutingStrategies . Bubble , handledEventsToo : true ) ;
246248
247- _popup = new Popup
249+ // Host the tree on a Flyout anchored to the toggle button, not a free popup: a flyout
250+ // positions itself in the trigger's own surface, so the dropdown stays correctly placed under
251+ // fractional display scaling. The Fluent presenter chrome is stripped so the panel's own thin
252+ // border is the only boundary the user sees.
253+ _flyout = new Flyout
248254 {
249- PlacementTarget = _dropdownButton ,
250255 Placement = PlacementMode . Bottom ,
251- IsLightDismissEnabled = true ,
252- // Zero footprint when closed so the layout tripwire never sees it overlapping the button.
253- Width = 0 ,
254- Height = 0 ,
255- HorizontalAlignment = HorizontalAlignment . Left ,
256- VerticalAlignment = VerticalAlignment . Top ,
257- Child = _popupPanel
256+ Content = _popupPanel ,
257+ FlyoutPresenterTheme = ChromelessPresenterTheme ( )
258258 } ;
259- AutomationProperties . SetAutomationId ( _popup , _automationId + ".Popup" ) ;
260- _popup . Opened += OnPopupOpened ;
261- _popup . Closed += OnPopupClosed ;
259+ _flyout . Opened += OnFlyoutOpened ;
260+ _flyout . Closed += OnFlyoutClosed ;
262261
263- var root = new Panel ( ) ;
264- root . Children . Add ( _dropdownButton ) ;
265- root . Children . Add ( _popup ) ;
266- Child = root ;
262+ // The chooser's own visual content is just the collapsed toggle button; the tree lives in the
263+ // flyout's separate top-level, shown on demand.
264+ Child = _dropdownButton ;
267265
268266 UpdateCollapsedLabel ( ) ;
269267 }
@@ -303,8 +301,8 @@ public string SelectedPosId
303301 /// <summary>The display name shown collapsed (the selected POS name, or the empty-row label).</summary>
304302 public string SelectedDisplayText => _dropdownLabel . Text ?? string . Empty ;
305303
306- /// <summary>True when the tree popup is currently open.</summary>
307- public bool IsOpen => _popup . IsOpen ;
304+ /// <summary>True when the tree flyout is currently open.</summary>
305+ public bool IsOpen => _open ;
308306
309307 /// <summary>The collapsed toggle button (the field-sized box the user clicks). For tests/hosts.</summary>
310308 public ToggleButton DropdownButton => _dropdownButton ;
@@ -319,16 +317,17 @@ public string SelectedPosId
319317 public ListBox FilteredList => _filterList ;
320318
321319 /// <summary>
322- /// Detaches and returns the on-top popup CONTENT panel (filter + tree/filter-list + create row)
323- /// for headless PNG capture only: a <see cref="Popup"/> renders in its own top-level, which the
324- /// host window's CaptureRenderedFrame does not include, so a snapshot of the chooser shows only the
325- /// collapsed box. Hosting THIS panel in a capture window renders the actual on-top tree the user
326- /// sees. The chooser is left non-functional afterward (the popup is emptied), so call it on a
327- /// throwaway instance — never in production.
320+ /// Detaches and returns the on-top flyout CONTENT panel (filter + tree/filter-list + create row)
321+ /// for headless PNG capture only: a flyout renders in its own top-level, which the host window's
322+ /// CaptureRenderedFrame does not include, so a snapshot of the chooser shows only the collapsed
323+ /// box. Hosting THIS panel in a capture window renders the actual on-top tree the user sees. The
324+ /// chooser is left non-functional afterward (the flyout is emptied), so call it on a throwaway
325+ /// instance — never in production.
328326 /// </summary>
329327 public Control DetachPopupContentForCapture ( )
330328 {
331- _popup . Child = null ;
329+ _flyout . Hide ( ) ;
330+ _flyout . Content = null ;
332331 return _popupPanel ;
333332 }
334333
@@ -478,13 +477,19 @@ private void CommitSelection(string id, string display, bool raise)
478477
479478 private void OnDropdownButtonCheckedChanged ( object sender , RoutedEventArgs e )
480479 {
481- _popup . IsOpen = _dropdownButton . IsChecked == true ;
480+ if ( _flyout == null )
481+ return ;
482+ if ( _dropdownButton . IsChecked == true )
483+ _flyout . ShowAt ( _dropdownButton ) ;
484+ else
485+ _flyout . Hide ( ) ;
482486 }
483487
484- private void OnPopupOpened ( object sender , EventArgs e )
488+ private void OnFlyoutOpened ( object sender , EventArgs e )
485489 {
486490 // Start each open with a clean filter (tree shown), then focus the filter box so typing filters
487- // immediately. Posted at Input priority so it runs after the popup's own layout/template.
491+ // immediately. Posted at Input priority so it runs after the flyout's own layout/template.
492+ _open = true ;
488493 if ( ! string . IsNullOrEmpty ( _filterBox . Text ) )
489494 _filterBox . Text = string . Empty ;
490495 else
@@ -493,15 +498,17 @@ private void OnPopupOpened(object sender, EventArgs e)
493498 Avalonia . Threading . DispatcherPriority . Input ) ;
494499 }
495500
496- private void OnPopupClosed ( object sender , EventArgs e )
501+ private void OnFlyoutClosed ( object sender , EventArgs e )
497502 {
503+ // Keep the toggle button in sync when the flyout closes by light-dismiss (click-away).
504+ _open = false ;
498505 if ( _dropdownButton . IsChecked == true )
499506 _dropdownButton . IsChecked = false ;
500507 }
501508
502509 private void CloseDropdown ( )
503510 {
504- _popup . IsOpen = false ;
511+ _flyout . Hide ( ) ;
505512 _dropdownButton . IsChecked = false ;
506513 }
507514
@@ -698,6 +705,23 @@ private static ControlTheme CompactListItemTheme()
698705 return theme ;
699706 }
700707
708+ // Strip the Fluent FlyoutPresenter's heavy grey padding/border/background to nothing, so the
709+ // tree panel's own thin border is the only boundary the user sees (mirrors the option-picker
710+ // flyout look, keeping the two dropdowns visually consistent).
711+ private static ControlTheme ChromelessPresenterTheme ( )
712+ {
713+ ControlTheme baseTheme = null ;
714+ if ( Application . Current != null
715+ && Application . Current . TryGetResource ( typeof ( FlyoutPresenter ) , null , out var found ) )
716+ baseTheme = found as ControlTheme ;
717+ var theme = new ControlTheme ( typeof ( FlyoutPresenter ) ) { BasedOn = baseTheme } ;
718+ theme . Setters . Add ( new Setter ( TemplatedControl . PaddingProperty , new Thickness ( 0 ) ) ) ;
719+ theme . Setters . Add ( new Setter ( TemplatedControl . BorderThicknessProperty , new Thickness ( 0 ) ) ) ;
720+ theme . Setters . Add ( new Setter ( TemplatedControl . BackgroundProperty , Brushes . Transparent ) ) ;
721+ theme . Setters . Add ( new Setter ( TemplatedControl . CornerRadiusProperty , new CornerRadius ( 0 ) ) ) ;
722+ return theme ;
723+ }
724+
701725 /// <summary>
702726 /// A tree node bound by the chooser's TreeView. Carries the expansion state (two-way bound to the
703727 /// container) and the children, mirroring ChooserTreeNode but POS-specific and LCModel-free.
0 commit comments