diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs
index ba8fd0e307..427620807f 100644
--- a/Terminal.Gui/ViewBase/View.Command.cs
+++ b/Terminal.Gui/ViewBase/View.Command.cs
@@ -296,19 +296,41 @@ private void SetupCommands ()
}
///
- /// Called when a command that has not been bound is invoked.
+ /// Called when a command that has not been bound (via AddCommand) is invoked on this View.
/// Set CommandEventArgs.Handled to and return to indicate the event was
/// handled and processing should stop.
///
+ ///
+ ///
+ /// This is part of the command bubbling pipeline. When a entry fires a command
+ /// that has no AddCommand handler, this method is called. If the command is in an ancestor's
+ /// list, will forward it to that ancestor.
+ ///
+ ///
+ /// Adding a entry without a corresponding AddCommand handler is
+ /// intentional and idiomatic — it signals that the command should bubble to an ancestor.
+ ///
+ ///
/// The event arguments.
/// to stop processing.
protected virtual bool OnCommandNotBound (CommandEventArgs args) => false;
///
- /// Cancelable event raised when a command that has not been bound is invoked.
+ /// Cancelable event raised when a command that has not been bound (via AddCommand) is invoked.
/// Set CommandEventArgs.Handled to to indicate the event was handled and processing should
/// stop.
///
+ ///
+ ///
+ /// This event fires as part of the command bubbling mechanism. A common use case is a SubView that binds
+ /// mouse-wheel events to scroll commands (via ) without adding a local handler.
+ /// The unhandled command fires this event, and if not cancelled here, forwards
+ /// the command to the nearest ancestor whose includes it.
+ ///
+ ///
+ /// See also: , .
+ ///
+ ///
public event EventHandler? CommandNotBound;
#region Accept
@@ -1256,17 +1278,32 @@ private static bool IsSourceWithinView (View target, ICommandContext? ctx)
///
/// Gets or sets the list of commands that should bubble up to this View from unhandled SubViews
- /// or from SubViews within this View's adornments (Padding, Border).
+ /// or from SubViews within this View's adornments (Padding, Border, Margin).
/// When a SubView raises a command that is not handled, and the command is in the SuperView's
/// list, the command will be invoked on the SuperView.
///
///
///
- /// For SubViews inside an (e.g., a button in Padding or Border),
- /// the bubble target is rather than .
+ /// For SubViews inside an (e.g., a view in Padding or Border),
+ /// the bubble target is (the owning View) rather than
+ /// . This means a view added to editor.Padding will bubble
+ /// commands directly to editor, not to the PaddingView.
+ ///
+ ///
+ /// Mouse-wheel forwarding pattern: A SubView can add a entry
+ /// (e.g., MouseBindings.Add(MouseFlags.WheeledUp, Command.ScrollUp)) without calling
+ /// AddCommand for the command. The unhandled command will fire
+ /// and then bubble via to the nearest
+ /// ancestor whose contains it.
+ ///
+ ///
+ /// Example — enable scroll command bubbling:
+ ///
+ /// editor.CommandsToBubbleUp = [Command.ScrollUp, Command.ScrollDown];
+ ///
///
///
- /// e.g. to enable bubbling for hierarchical views:
+ /// Example — enable bubbling for hierarchical views:
///
/// menuBar.CommandsToBubbleUp = [Command.Activate];
///
diff --git a/docfx/docs/command-diagrams.md b/docfx/docs/command-diagrams.md
index 08657ce9bd..84e1f8ec18 100644
--- a/docfx/docs/command-diagrams.md
+++ b/docfx/docs/command-diagrams.md
@@ -130,3 +130,39 @@ flowchart TD
- holds a (not a `SubMenu`). holds a `SubMenu` (a nested ).
- connects non-containment boundaries (e.g., ↔ ) so / from the remote view re-enters the owner's full command pipeline with `Routing = Bridged`.
- uses consume dispatch ( = true, → `Focused`) — inner activations are consumed and do not propagate to 's SuperView.
+
+### Level 4: Mouse Event Forwarding via CommandNotBound Bubbling
+
+This diagram shows how a child view can forward mouse-wheel events to an ancestor via the `CommandNotBound` → `TryBubbleUp` mechanism. The child adds a `MouseBinding` without an `AddCommand` handler, causing the command to bubble.
+
+```mermaid
+flowchart TD
+ input["Mouse wheel on Gutter (in Editor.Padding)"] --> mb["MouseBindings maps
WheeledUp → Command.ScrollUp"]
+ mb --> invoke["Gutter.InvokeCommand(ScrollUp)"]
+ invoke --> lookup{"AddCommand handler
exists for ScrollUp?"}
+
+ lookup --> |"no"| notbound["DefaultCommandNotBoundHandler"]
+ notbound --> raise["RaiseCommandNotBound:
OnCommandNotBound (virtual)
→ CommandNotBound event"]
+ raise --> |"not handled"| bubble["TryBubbleUp"]
+
+ bubble --> check_sv{"SuperView is
AdornmentView?"}
+ check_sv --> |"yes (Gutter in Padding)"| adorn_check{"Adornment.Parent
.CommandsToBubbleUp
contains ScrollUp?"}
+ adorn_check --> |"yes"| parent_invoke["Editor.InvokeCommand(ScrollUp)
(Routing = BubblingUp)"]
+ parent_invoke --> editor_handler["Editor.ScrollVertical(-1)
→ returns true"]
+
+ check_sv --> |"no (normal SubView)"| sv_check{"SuperView
.CommandsToBubbleUp
contains ScrollUp?"}
+ sv_check --> |"yes"| sv_invoke["SuperView.InvokeCommand(ScrollUp)
(Routing = BubblingUp)"]
+ sv_check --> |"no"| willbubble{"CommandWillBubbleToAncestor?"}
+ willbubble --> |"yes"| ret_handled["return true (consumed)"]
+ willbubble --> |"no"| ret_null["return null (unhandled)"]
+
+ adorn_check --> |"no"| willbubble
+ raise --> |"handled (args.Handled = true)"| ret_true["return true (stop)"]
+ lookup --> |"yes"| exec["Execute handler directly"]
+```
+
+**Key Points:**
+- Adding a `MouseBinding` without a corresponding `AddCommand` handler is **intentional and idiomatic** — it declares that the command should bubble to an ancestor.
+- For views inside an (Padding, Border, Margin), the bubble target is `Adornment.Parent` (the owning View), not `SuperView`.
+- checks both the `SuperView` path and the `AdornmentView` path.
+- The event can intercept and cancel bubbling by setting `args.Handled = true`.
diff --git a/docfx/docs/mouse.md b/docfx/docs/mouse.md
index c331ff2f61..49418feddd 100644
--- a/docfx/docs/mouse.md
+++ b/docfx/docs/mouse.md
@@ -604,6 +604,79 @@ Platform API ? InputProcessorImpl ? AnsiResponseParser ? MouseInterpreter ? Appl
This ensures consistent mouse behavior across platforms while maintaining platform-specific optimizations.
+## Mouse Event Forwarding via Command Bubbling
+
+A common need is forwarding mouse-wheel events from a child view to an ancestor (e.g., a gutter subview inside a scrollable editor's Padding that should scroll the editor). Terminal.Gui supports this idiomatically via the **CommandNotBound bubbling** mechanism.
+
+### The Pattern
+
+1. **Child view**: Add a `MouseBinding` for the wheel event **without** calling `AddCommand` for that command.
+2. **Parent view**: Include the scroll commands in .
+3. **Result**: When the child receives the wheel event, the mouse binding fires the command. Because the child has no handler (`AddCommand` was not called), `DefaultCommandNotBoundHandler` runs → finds the ancestor with the command in `CommandsToBubbleUp` → invokes it on the ancestor.
+
+### Example: Gutter Forwards Wheel to Editor
+
+```csharp
+// Parent (Editor) handles scroll commands and opts into bubbling
+public class Editor : View
+{
+ public Editor ()
+ {
+ AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
+ AddCommand (Command.ScrollDown, () => ScrollVertical (1));
+
+ // Allow scroll commands from SubViews/adornment SubViews to bubble here
+ CommandsToBubbleUp = [Command.ScrollUp, Command.ScrollDown];
+ }
+}
+
+// Child (Gutter) binds wheel events but does NOT add command handlers
+public class Gutter : View
+{
+ public Gutter ()
+ {
+ // Bind mouse wheel to scroll commands — no AddCommand needed.
+ // The unhandled command will bubble up to the nearest ancestor
+ // whose CommandsToBubbleUp includes it.
+ MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
+ MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
+ }
+}
+```
+
+### How It Works
+
+```
+Mouse wheel on Gutter
+ → MouseBindings maps WheeledUp → Command.ScrollUp
+ → Gutter.InvokeCommand(ScrollUp)
+ → No handler found (AddCommand was never called)
+ → DefaultCommandNotBoundHandler runs
+ → RaiseCommandNotBound → TryBubbleUp
+ → Finds ancestor (Editor) with Command.ScrollUp in CommandsToBubbleUp
+ → Editor.InvokeCommand(ScrollUp) → scrolls
+```
+
+### AdornmentView Special Case
+
+For views hosted inside an adornment (Padding, Border, or Margin), the bubble target is the **adornment's Parent** (the owning View), not the `SuperView`. This means a view added to `editor.Padding` will bubble commands directly to `editor`, skipping the `PaddingView` intermediary.
+
+```csharp
+// Gutter added to Editor's Padding — bubbles to Editor automatically
+editor.Padding.Add (gutter);
+```
+
+This is handled internally by and .
+
+### When to Use This Pattern
+
+* A SubView should forward wheel/scroll events to its container without handling them itself.
+* A view in Padding/Border needs to delegate commands to the owning View.
+* You want clean separation: the child declares *what* user gesture maps to *which* command, and the ancestor decides *how* to handle it.
+
+> [!TIP]
+> Adding a `MouseBinding` without a corresponding `AddCommand` handler is **intentional and idiomatic**. It signals that the command should bubble to an ancestor rather than being handled locally. See .
+
## Best Practices
* **Use Mouse Bindings and Commands** for simple interactions - integrates with keyboard bindings