You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add dismiss-on-outside-click for portals and fix right-click cursor in MultilineEdit
Portal system: add opt-in DismissOnOutsideClick property to IHasPortalBounds
and PortalContentBase. Portals with this enabled are automatically dismissed
on outside click and window deactivation, with a DismissRequested event for
cleanup. Defaults to false so existing portals are unaffected.
MultilineEdit: move cursor to click position before firing MouseRightClick,
matching standard editor behavior.
Copy file name to clipboardExpand all lines: docs/PORTAL_SYSTEM.md
+57-1Lines changed: 57 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,6 +14,7 @@ SharpConsoleUI's portal system enables overlay rendering — content that floats
14
14
-[Building a Custom Portal](#building-a-custom-portal)
15
15
-[Mouse and Keyboard Routing](#mouse-and-keyboard-routing)
16
16
-[Nested Containers](#nested-containers)
17
+
-[Dismiss on Outside Click](#dismiss-on-outside-click)
17
18
-[Lifecycle Management](#lifecycle-management)
18
19
-[Quick Reference](#quick-reference)
19
20
@@ -400,6 +401,61 @@ This works because:
400
401
-**Mouse**: Portal forwards to child, child does its own internal hit-testing for nested children
401
402
-**Focus**: `IFocusTrackingContainer` ensures focus tracking through nesting
402
403
404
+
## Dismiss on Outside Click
405
+
406
+
Portals can opt in to automatic dismissal when the user clicks outside their bounds. This is useful for dropdowns, context menus, and other transient overlays.
407
+
408
+
### Enabling
409
+
410
+
Set `DismissOnOutsideClick` on the portal content:
411
+
412
+
```csharp
413
+
// Via PortalContentBase subclass
414
+
varportal=newPortalContentContainer();
415
+
portal.DismissOnOutsideClick=true;
416
+
417
+
// Or via IHasPortalBounds (default-implemented as false)
418
+
publicboolDismissOnOutsideClick=>true;
419
+
```
420
+
421
+
### DismissRequested Event
422
+
423
+
`PortalContentBase` exposes a `DismissRequested` event that fires **before** the portal is removed. Use it for cleanup:
424
+
425
+
```csharp
426
+
portal.DismissRequested+= (sender, e) =>
427
+
{
428
+
// Clean up state, close related UI, etc.
429
+
_portalNode=null;
430
+
_portal=null;
431
+
};
432
+
portal.DismissOnOutsideClick=true;
433
+
434
+
_portalNode=window.CreatePortal(this, portal);
435
+
```
436
+
437
+
### How It Works
438
+
439
+
Portals with `DismissOnOutsideClick = true` are dismissed in two scenarios:
440
+
441
+
**Outside click:**
442
+
1. On left-click or right-click, `WindowEventDispatcher` walks all portal nodes
443
+
2. For each matching portal, it checks if the click is outside `GetPortalBounds()`
444
+
3. Matching portals are collected first (to avoid modifying the tree during iteration)
445
+
4.`DismissRequested` fires on each, then `RemovePortal()` removes it
446
+
5. Normal click processing continues — the click is **not** consumed
447
+
448
+
**Window deactivation:**
449
+
1. When the window loses focus (another window is activated, or the user clicks the desktop)
450
+
2. All portals with `DismissOnOutsideClick = true` are dismissed immediately
451
+
3.`DismissRequested` fires before removal, same as outside-click dismissal
452
+
453
+
### Defaults
454
+
455
+
-`IHasPortalBounds.DismissOnOutsideClick` defaults to `false` (default interface implementation)
456
+
-`PortalContentBase.DismissOnOutsideClick` is a settable property, also defaulting to `false`
457
+
- Existing portals are completely unaffected unless they explicitly opt in
458
+
403
459
## Lifecycle Management
404
460
405
461
1.**Create** portal content and set bounds
@@ -418,7 +474,7 @@ Always remove portals before disposing the owner control. Portals that outlive t
418
474
|`Window.RemovePortal()`|`Window.cs`| Remove a portal overlay |
419
475
|`PortalContentBase`|`Controls/PortalContentBase.cs`| Abstract base for portal content |
420
476
|`PortalContentContainer`|`Controls/PortalContentContainer.cs`| Container for child controls in portals |
421
-
|`IHasPortalBounds`|`Controls/IHasPortalBounds.cs`| Interface for portal bounds |
477
+
|`IHasPortalBounds`|`Layout/IHasPortalBounds.cs`| Interface for portal bounds and dismiss opt-in|
422
478
|`PortalPositioner`|`Layout/PortalPositioner.cs`| Smart positioning with flip/clamp |
423
479
|`PortalPositionRequest`|`Layout/PortalPositioner.cs`| Positioning request record |
424
480
|`PortalPositionResult`|`Layout/PortalPositioner.cs`| Positioning result record |
0 commit comments