Rename IApplication.Current to TopRunnable

Co-authored-by: tig <585482+tig@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-20 19:34:48 +00:00
parent 16b42e86fd
commit 4b975fd5b7
90 changed files with 916 additions and 820 deletions

View File

@@ -192,22 +192,22 @@ public interface IApplication
Terminal.Gui v2 modernized its terminology for clarity:
### Application.Current (formerly "Top")
### Application.TopRunnable (formerly "Current", and before that "Top")
The `Current` property represents the currently running Toplevel (the active session):
The `TopRunnable` property represents the Toplevel on the top of the session stack (the active runnable session):
```csharp
// Access the current session
Toplevel? current = app.Current;
// Access the top runnable session
Toplevel? topRunnable = app.TopRunnable;
// From within a view
Toplevel? current = App?.Current;
Toplevel? topRunnable = App?.TopRunnable;
```
**Why "Current" instead of "Top"?**
- Follows .NET patterns (`Thread.CurrentThread`, `HttpContext.Current`)
- Self-documenting: immediately clear it's the "current" active view
- Less confusing than "Top" which could mean "topmost in Z-order"
**Why "TopRunnable"?**
- Clearly indicates it's the top of the runnable session stack
- Aligns with the IRunnable architecture proposal
- Distinguishes from other concepts like "Current" which could be ambiguous
### Application.SessionStack (formerly "TopLevels")
@@ -256,7 +256,7 @@ public static partial class Application
// OLD:
void MyMethod()
{
Application.Current?.SetNeedsDraw();
Application.TopRunnable?.SetNeedsDraw();
}
// NEW:
@@ -467,7 +467,7 @@ public void Refresh()
AVOID:
public void Refresh()
{
Application.Current?.SetNeedsDraw(); // Obsolete!
Application.TopRunnable?.SetNeedsDraw(); // Obsolete!
}
```
@@ -487,7 +487,7 @@ public class Service
AVOID (obsolete pattern):
public void Refresh()
{
Application.Current?.SetNeedsDraw(); // Obsolete static access
Application.TopRunnable?.SetNeedsDraw(); // Obsolete static access
}
PREFERRED:

View File

@@ -85,12 +85,12 @@ When measuring the screen space taken up by a `string` you can use the extension
In v1, @Terminal.Gui.View was derived from `Responder` which supported `IDisposable`. In v2, `Responder` has been removed and @Terminal.Gui.View is the base-class supporting `IDisposable`.
In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.Current](~/api/Terminal.Gui.Application.Current. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.Current; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run<myTopLevel>`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting.
In v1, @Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.TopRunnable](~/api/Terminal.Gui.Application.TopRunnable. In v2, @Terminal.Gui.App.Application.Init no longer automatically creates a toplevel or sets @Terminal.Gui.App.Application.TopRunnable; app developers must explicitly create the toplevel view and pass it to @Terminal.Gui.App.Application.Run (or use `Application.Run<myTopLevel>`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting.
### How to Fix
* Replace `Responder` with @Terminal.Gui.View
* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Current`.
* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.TopRunnable`.
* Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited.
## Instance-Based Application Architecture
@@ -144,7 +144,7 @@ When accessing application services from within views, use the `App` property in
// OLD (v1 / obsolete static):
public void Refresh()
{
Application.Current?.SetNeedsDraw();
Application.TopRunnable?.SetNeedsDraw();
}
// NEW (v2 - use View.App):
@@ -591,6 +591,6 @@ new (
* To simplify programming, any `View` added as a SubView another `View` will have it's lifecycle owned by the Superview; when a `View` is disposed, it will call `Dispose` on all the items in the `SubViews` property. Note this behavior is the same as it was in v1, just clarified.
* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.Current (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.Current` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.Current` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run<T> ()`.
* In v1, `Application.End` called `Dispose ()` on @Terminal.Gui.App.Application.TopRunnable (via `Runstate.Toplevel`). This was incorrect as it meant that after `Application.Run` returned, `Application.TopRunnable` had been disposed, and any code that wanted to interrogate the results of `Run` by accessing `Application.TopRunnable` only worked by accident. This is because GC had not actually happened; if it had the application would have crashed. In v2 `Application.End` does NOT call `Dispose`, and it is the caller to `Application.Run` who is responsible for disposing the `Toplevel` that was either passed to `Application.Run (View)` or created by `Application.Run<T> ()`.
* Any code that creates a `Toplevel`, either by using `top = new()` or by calling either `top = Application.Run ()` or `top = ApplicationRun<T>()` must call `top.Dispose` when complete. The exception to this is if `top` is passed to `myView.Add(top)` making it a subview of `myView`. This is because the semantics of `Add` are that the `myView` takes over responsibility for the subviews lifetimes. Of course, if someone calls `myView.Remove(top)` to remove said subview, they then re-take responsbility for `top`'s lifetime and they must call `top.Dispose`.

View File

@@ -181,7 +181,7 @@ return app.Current?.AdvanceFocus (direction, behavior);
This method is called from the `Command` handlers bound to the application-scoped keybindings created during `app.Init()`. It is `public` as a convenience.
**Note:** When accessing from within a View, use `App?.Current` instead of `Application.Current` (which is obsolete).
**Note:** When accessing from within a View, use `App?.Current` instead of `Application.TopRunnable` (which is obsolete).
This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`).
@@ -379,7 +379,7 @@ In v1 `View` had `MostFocused` property that traversed up the view-hierarchy ret
var focused = Application.Navigation.GetFocused();
// This replaces the v1 pattern:
// var focused = Application.Current.MostFocused;
// var focused = Application.TopRunnable.MostFocused;
```
## How Does `View.Add/Remove` Work?

View File

@@ -0,0 +1,95 @@
# IRunnable Architecture Proposal
**Status**: Proposal
**Version**: 1.6 (Property-Based Architecture)
**Date**: 2025-01-20
## Summary
This proposal recommends decoupling Terminal.Gui's "Runnable" concept from `Toplevel` and `ViewArrangement.Overlapped`, elevating it to a first-class interface-based abstraction.
**Key Insight**: Analysis of the codebase reveals that **all runnable sessions are effectively modal** - they block in `Application.Run()` until stopped and capture input. The distinction between "modal" and "non-modal" in the current design is artificial:
- The `Modal` property only affects input propagation and Z-order, not the fundamental run loop behavior
- All `Toplevel`s block in `Run()` - there's no "background" runnable concept
- Non-modal `Toplevel`s (like `WizardAsView`) are just embedded views with `Modal = false`, not true sessions
- Overlapped windows are managed by `ViewArrangement.Overlapped`, not runnability
By introducing `IRunnable`, we create a clean separation where:
- **Runnable** = Can be run as a **UI**-blocking session with `Application.Run()` and returns a result
- **Overlapped** = `ViewArrangement.Overlapped` for window management (orthogonal to runnability)
- **Embedded** = Just views, not runnable at all
## Terminology
This proposal introduces new terminology to clarify the architecture:
| Term | Definition |
|------|------------|
| **`IRunnable`** | Base interface for Views capable of running as an independent session with `Application.Run()` without returning a result. Replaces `Toplevel` as the contract for runnable views. When an `IRunnable` is passed to `IApplication.Run`, `Run` blocks until the `IRunnable` `Stops`. |
| **`IRunnable<TResult>`** | Generic interface derived from `IRunnable` that can return a typed result. |
| **`Runnable`** | Optional base class that implements `IRunnable` and derives from `View`, providing default lifecycle behavior. Views can derive from this or implement `IRunnable` directly. |
| **`TResult`** | Type parameter specifying the type of result data returned when the runnable completes (e.g., `int` for button index, `string` for file path, enum, or other complex type). `Result` is `null` if the runnable stopped without the user explicitly accepting it (ESC pressed, window closed, etc.). |
| **`Result`** | Property on `IRunnable<TResult>` that holds the typed result data. Should be set in `IsRunningChanging` handler (when `newValue = false`) **before** the runnable is popped from `RunnableSessionStack`. This allows subscribers to inspect results and optionally cancel the stop. Available after `IApplication.Run` returns. `null` indicates cancellation/non-acceptance. |
| **RunnableSession** | A running instance of an `IRunnable`. Managed by `IApplication` via `Begin()`, `Run()`, `RequestStop()`, and `End()` methods. Represented by a `RunnableSessionToken` on the `RunnableSessionStack`. |
| **`RunnableSessionToken`** | Object returned by `Begin()` that represents a running session. Wraps an `IRunnable` instance (via a `Runnable` property) and is stored in `RunnableSessionStack`. Disposed when session ends. |
| **`RunnableSessionStack`** | A stack of `RunnableSessionToken` instances, each wrapping an `IRunnable`. Tracks all running runnables in the application. Literally a `ConcurrentStack<IRunnable>`. Replaces `SessionStack` (formerly `Toplevels`). |
| **`IsRunning`** | Boolean property on `IRunnable` indicating whether the runnable is currently on the `RunnableSessionStack` (i.e., `RunnableSessionStack.Any(token => token.Runnable == this)`). Read-only, derived from stack state. Runnables are added during `IApplication.Begin` and removed in `IApplication.End`. Replaces `Toplevel.Running`. |
| **`IsRunningChanging`** | Cancellable event raised **before** an `IRunnable` is added to or removed from `RunnableSessionStack`. When transitioning to `IsRunning = true`, can be canceled to prevent starting. When transitioning to `IsRunning = false`, allows code to prevent closure (e.g., prompt to save changes) AND is the ideal place to extract `Result` before the runnable is removed from the stack. Event args (`CancelEventArgs<bool>`) provide the new state in `NewValue`. Replaces `Toplevel.Closing` and partially `Toplevel.Activate`. |
| **`IsRunningChanged`** | Non-cancellable event raised **after** a runnable has been added to or removed from `RunnableSessionStack`. Fired after `IsRunning` has changed to the new value (true = started, false = stopped). For post-state-change logic (e.g., setting focus after start, cleanup after stop). Replaces `Toplevel.Activated` and `Toplevel.Closed`. |
| **`IsInitialized`** (`View` property) | Boolean property (on `View`) indicating whether a view has completed two-phase initialization (`View.BeginInit/View.EndInit`). From .NET's `ISupportInitialize` pattern. If the `IRunnable.IsInitialized == false`, `BeginInit` is called from `IApplication.Begin` after `IsRunning` has changed to `true`. `EndInit` is called immediately after `BeginInit`. |
| **`Initialized`** (`View` event) | Non-cancellable event raised as `View.EndInit()` completes. |
| **`TopRunnable`** (`IApplication` property) | The `IRunnable` that is on the top of the `RunnableSessionStack` stack. By definition and per-implementation, this `IRunnable` is capturing all mouse and keyboard input and is thus "Modal". Note: any other `IRunnable` instances on `RunnableSessionStack` continue to be laid out, drawn, and receive iteration events; they just don't get any user input. **Renamed from `Current`** to better reflect its purpose as the top runnable in the stack. Synonymous with the runnable having `IsModal = true`. |
| **`IsModal`** | Boolean property on `IRunnable` indicating whether the `IRunnable` is at the top of the `RunnableSessionStack` (i.e., `this == app.TopRunnable` or `app.RunnableSessionStack.Peek().Runnable == this`). The `IRunnable` at the top of the stack gets all mouse/keyboard input and thus is running "modally". Read-only, derived from stack state. `IsModal` represents the concept from the end-user's perspective. |
| **`IsModalChanging`** | Cancellable event raised **before** an `IRunnable` transitions to/from the top of the `RunnableSessionStack`. When becoming modal (`newValue = true`), can be canceled to prevent activation. Event args (`CancelEventArgs<bool>`) provide the new state. Replaces `Toplevel.Activate` and `Toplevel.Deactivate`. |
| **`IsModalChanged`** | Non-cancellable event raised **after** an `IRunnable` has transitioned to/from the top of the `RunnableSessionStack`. Fired after `IsModal` has changed to the new value (true = became modal, false = no longer modal). For post-activation logic (e.g., setting focus, updating UI state). Replaces `Toplevel.Activated` and `Toplevel.Deactivated`. |
| **`End`** (`IApplication` method) | Ends a running `IRunnable` instance by removing its `RunnableSessionToken` from the `RunnableSessionStack`. `IsRunningChanging` with `newValue = false` is raised **before** the token is popped from the stack (allowing result extraction and cancellation). `IsRunningChanged` is raised **after** the `Pop` operation. Then, `RunnableSessionStack.Peek()` is called to see if another `IRunnable` instance can transition to `IApplication.TopRunnable`/`IRunnable.IsModal = true`. |
| **`ViewArrangement.Overlapped`** | Layout mode for windows that can overlap with Z-order management. Orthogonal to runnability - overlapped windows can be embedded views (not runnable) or runnable sessions. |
**Key Architectural Changes:**
- **Simplified**: One interface `IRunnable` replaces both `Toplevel` and the artificial `Modal` property distinction
- **All sessions block**: No concept of "non-modal runnable" - if it's runnable, `Run()` blocks until `RequestStop()`
- **Type-safe results**: Generic `TResult` parameter provides compile-time type safety
- **Decoupled from layout**: Being runnable is independent of `ViewArrangement.Overlapped`
- **Consistent patterns**: All lifecycle events follow Terminal.Gui's Cancellable Work Pattern
- **Result extraction in `Stopping`**: `OnStopping()` is the correct place to extract `Result` before disposal
## Implementation Status
### Completed Work
- [x] **2025-11-20**: Renamed `IApplication.Current` to `IApplication.TopRunnable` to better reflect its role as the top runnable in the session stack
- Updated interface definition in `IApplication.cs`
- Updated implementation in `ApplicationImpl.cs`
- Updated static property in `Application.Current.cs`
- Updated all references in library code (28 occurrences)
- Updated all references in examples (50+ occurrences)
- Updated all references in tests (607 occurrences)
- Updated `View.IsCurrentTop` to use the renamed property
- Updated API documentation comments
- All tests pass
- No new warnings introduced
### Remaining Work
The following items from the original proposal are still pending:
- [ ] Implement `IRunnable` non-generic base interface
- [ ] Implement `IRunnable<TResult>` generic interface
- [ ] Create optional `Runnable` base class
- [ ] Replace `SessionToken` with `RunnableSessionToken`
- [ ] Replace `SessionStack` (ConcurrentStack<Toplevel>) with `RunnableSessionStack` (ConcurrentStack<IRunnable>)
- [ ] Add lifecycle events: `IsRunningChanging`, `IsRunningChanged`, `IsModalChanging`, `IsModalChanged`
- [ ] Migrate `Toplevel` to implement `IRunnable`
- [ ] Update all view classes to use new pattern
- [ ] Add comprehensive tests for new architecture
- [ ] Update all documentation
## See Also
- [Original Issue #4148](https://github.com/gui-cs/Terminal.Gui/issues/4148)
- [Application Lifecycle Documentation](application.md)
- [View Documentation](View.md)