This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-23672-tui-diagram
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 189fb63b6c3e9977a02b90473371b77d8b00e6c7
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jun 4 10:56:49 2026 +0200

    CAMEL-23672: camel-tui - Unify routes tab diagram with topology drill-down
    
    Replace legacy ASCII diagram in the Routes tab with the same native
    topology and route drill-down experience from the Diagram tab. Users
    now get consistent navigation (arrow keys, Enter to drill down, Esc
    to go back, t for topology) across both tabs. Drop the a (all/single)
    diagram toggle.
    
    Also swap F2 example browser keys so r runs directly and Enter opens
    the tweak dialog, and move the run options dialog higher up.
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |   8 +-
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 860 ++++++++++++++++-----
 .../jbang/core/commands/tui/RunOptionsForm.java    |   4 +-
 3 files changed, 669 insertions(+), 203 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 2404676351ce..8f9a52013245 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -502,11 +502,11 @@ class ActionsPopup {
             } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
                 navigateExampleBrowser(10);
             } else if (ke.isChar('r')) {
-                openNameInput();
+                launchSelectedExample();
             } else if (ke.isChar('d')) {
                 loadDocFromExample();
             } else if (ke.isConfirm()) {
-                launchSelectedExample();
+                openNameInput();
             }
             return true;
         }
@@ -687,8 +687,8 @@ class ActionsPopup {
         }
         if (showExampleBrowser) {
             hint(spans, "↑↓", "navigate");
-            hint(spans, "Enter", "run");
-            hint(spans, "r", "run...");
+            hint(spans, "r", "run");
+            hint(spans, "Enter", "run...");
             hint(spans, "d", "docs");
             hintLast(spans, "Esc", "back");
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 0b61a0283652..e23ac133d74d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -17,7 +17,9 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Path;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.List;
 
 import dev.tamboui.layout.Constraint;
@@ -38,6 +40,7 @@ import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -62,15 +65,16 @@ class RoutesTab implements MonitorTab {
     // Table states
     private final TableState routeTableState = new TableState();
     private final TableState processorTableState = new TableState();
-    private final TableState routeHeaderTableState = new TableState();
 
     // Diagram support (shared rendering/loading logic)
     private final DiagramSupport diagram = new DiagramSupport();
     private final SourceViewer sourceViewer = new SourceViewer();
-    private boolean diagramAllRoutes;
     private boolean diagramMetrics = true;
     private boolean showDescription;
-    private String diagramRouteId;
+    private boolean showExternal;
+    private boolean topologyMode = true;
+    private String drillDownRouteId;
+    private final Deque<String> routeNavigationStack = new ArrayDeque<>();
 
     RoutesTab(MonitorContext ctx) {
         this.ctx = ctx;
@@ -88,38 +92,185 @@ class RoutesTab implements MonitorTab {
         return diagramMetrics;
     }
 
-    boolean isDiagramAllRoutes() {
-        return diagramAllRoutes;
-    }
-
     boolean isShowSource() {
         return sourceViewer.isVisible();
     }
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
-        // Source view scrolling
+        // Source view scrolling (takes priority when active)
         if (sourceViewer.handleKeyEvent(ke)) {
             return true;
         }
 
-        // Diagram scrolling
+        // Source viewer toggle (drill-down mode)
+        if (!topologyMode && diagram.isShowDiagram() && ke.isChar('c')) {
+            loadSourceForSelectedNode();
+            return true;
+        }
+
+        // Topology node navigation
+        if (diagram.isShowDiagram() && topologyMode && diagram.hasDiagramData()
+                && !diagram.getNodeBoxes().isEmpty()) {
+            if (ke.isUp()) {
+                diagram.selectNodeUp();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+            if (ke.isDown()) {
+                diagram.selectNodeDown();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+            if (ke.isLeft()) {
+                diagram.selectNodeLeft();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+            if (ke.isRight()) {
+                diagram.selectNodeRight();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+            if (ke.isHome()) {
+                diagram.selectFirstNode();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+            if (ke.isEnd()) {
+                diagram.selectLastNode();
+                diagram.scrollToSelectedNode();
+                return true;
+            }
+        }
+
+        // EIP node navigation in route drill-down mode
+        if (diagram.isShowDiagram() && !topologyMode && 
!diagram.getEipNodeBoxes().isEmpty()) {
+            if (ke.isUp()) {
+                diagram.selectEipNodeUp();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+            if (ke.isDown()) {
+                diagram.selectEipNodeDown();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+            if (ke.isLeft()) {
+                diagram.selectEipNodeLeft();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+            if (ke.isRight()) {
+                diagram.selectEipNodeRight();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+            if (ke.isHome()) {
+                diagram.selectFirstEipNode();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+            if (ke.isEnd()) {
+                diagram.selectLastEipNode();
+                diagram.scrollToSelectedEipNode();
+                return true;
+            }
+        }
+
+        // Jump back to topology from drill-down
+        if (diagram.isShowDiagram() && !topologyMode && ke.isChar('t')) {
+            routeNavigationStack.clear();
+            diagram.setPendingSelectionRouteId(drillDownRouteId);
+            drillDownRouteId = null;
+            topologyMode = true;
+            diagram.setTopologyMode(true);
+            diagram.setSelectedEipNodeIndex(-1);
+            diagram.resetScroll();
+            if (diagram.hasNativeLayout()) {
+                return true;
+            }
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
+
+        // Diagram scrolling (PgUp/PgDn etc)
         if (diagram.handleScrollKeys(ke)) {
             return true;
         }
 
-        // Diagram-specific controls (only when diagram is showing)
-        if (diagram.isShowDiagram()) {
-            if (ke.isCharIgnoreCase('m')) {
-                diagramMetrics = !diagramMetrics;
-                diagram.endLoad();
-                reloadDiagramQuietly();
+        // Toggle metrics (diagram mode)
+        if (diagram.isShowDiagram() && ke.isCharIgnoreCase('m')) {
+            diagramMetrics = !diagramMetrics;
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
+
+        // Toggle external systems (topology mode only)
+        if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
+            showExternal = !showExternal;
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
+
+        // Toggle description (diagram mode)
+        if (diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
+            diagram.setShowDescription(!diagram.isShowDescription());
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
+
+        // Jump to linked route from EIP node (Enter in route mode)
+        if (diagram.isShowDiagram() && !topologyMode && ke.isConfirm() && 
!diagram.getEipNodeBoxes().isEmpty()) {
+            String linkedRouteId = diagram.findLinkedRouteId(drillDownRouteId);
+            if (linkedRouteId != null && diagram.getRouteLayout(linkedRouteId) 
!= null) {
+                if (linkedRouteId.equals(drillDownRouteId)) {
+                    return true;
+                }
+                if (routeNavigationStack.contains(linkedRouteId)) {
+                    while (!routeNavigationStack.isEmpty() && 
!linkedRouteId.equals(routeNavigationStack.peek())) {
+                        routeNavigationStack.pop();
+                    }
+                    routeNavigationStack.pop();
+                } else {
+                    routeNavigationStack.push(drillDownRouteId);
+                }
+                drillDownRouteId = linkedRouteId;
+                diagram.selectFromNode(linkedRouteId);
+                diagram.resetScroll();
                 return true;
             }
+            return true;
+        }
+
+        // Drill down into route diagram (Enter from topology)
+        if (diagram.isShowDiagram() && topologyMode && ke.isConfirm()) {
+            String selectedRouteId = diagram.getSelectedRouteId();
+            if (selectedRouteId != null) {
+                IntegrationInfo info = ctx.findSelectedIntegration();
+                if (info != null && info.routes.stream().anyMatch(r -> 
selectedRouteId.equals(r.routeId))) {
+                    routeNavigationStack.clear();
+                    drillDownRouteId = selectedRouteId;
+                    topologyMode = false;
+                    diagram.setTopologyMode(false);
+                    diagram.selectFromNode(selectedRouteId);
+                    diagram.resetScroll();
+                    diagram.endLoad();
+                    if (diagram.getRouteLayout(selectedRouteId) != null) {
+                        return true;
+                    }
+                    reloadDiagram();
+                }
+            }
+            return true;
         }
 
-        // Sort
-        if (ke.isChar('s')) {
+        // Sort (only when not in diagram)
+        if (!diagram.isShowDiagram() && ke.isChar('s')) {
             if (routeTopMode) {
                 routeTopSortIndex = (routeTopSortIndex + 1) % 
ROUTE_TOP_SORT_COLUMNS.length;
                 routeTopSort = ROUTE_TOP_SORT_COLUMNS[routeTopSortIndex];
@@ -131,7 +282,7 @@ class RoutesTab implements MonitorTab {
             }
             return true;
         }
-        if (ke.isChar('S')) {
+        if (!diagram.isShowDiagram() && ke.isChar('S')) {
             if (routeTopMode) {
                 routeTopSortReversed = !routeTopSortReversed;
             } else {
@@ -146,34 +297,24 @@ class RoutesTab implements MonitorTab {
             return true;
         }
 
-        // Toggle all routes diagram flag
-        if (!sourceViewer.isVisible() && !diagram.isShowDiagram() && 
ke.isCharIgnoreCase('a')) {
-            diagramAllRoutes = !diagramAllRoutes;
-            return true;
-        }
-
-        // Toggle description in route table
+        // Toggle description in route table (only when not in diagram)
         if (!sourceViewer.isVisible() && !diagram.isShowDiagram() && 
ke.isCharIgnoreCase('n')) {
             showDescription = !showDescription;
             return true;
         }
 
-        // Text diagram toggle
+        // Diagram toggle
         if (ke.isCharIgnoreCase('d')) {
-            diagram.toggleDiagram(this::loadDiagramForSelectedRoute);
-            return true;
-        }
-
-        // Toggle description in diagram
-        if (diagram.isShowDiagram() && ke.isCharIgnoreCase('n')) {
-            diagram.setShowDescription(!diagram.isShowDescription());
-            diagram.endLoad();
-            loadDiagramForSelectedRoute();
+            if (diagram.isShowDiagram()) {
+                closeDiagram();
+            } else {
+                openDiagram();
+            }
             return true;
         }
 
-        // Source viewer toggle
-        if (ke.isChar('c')) {
+        // Source viewer toggle (table mode — route source)
+        if (!diagram.isShowDiagram() && ke.isChar('c')) {
             if (sourceViewer.isVisible()) {
                 sourceViewer.hide();
             } else {
@@ -203,24 +344,56 @@ class RoutesTab implements MonitorTab {
             sourceViewer.hide();
             return true;
         }
-        return diagram.handleEscape();
+        if (diagram.isShowDiagram()) {
+            if (!topologyMode) {
+                if (!routeNavigationStack.isEmpty()) {
+                    drillDownRouteId = routeNavigationStack.pop();
+                    diagram.selectFromNode(drillDownRouteId);
+                    diagram.resetScroll();
+                    return true;
+                }
+                // Go back to topology
+                diagram.setPendingSelectionRouteId(drillDownRouteId);
+                topologyMode = true;
+                diagram.setTopologyMode(true);
+                diagram.setSelectedEipNodeIndex(-1);
+                diagram.resetScroll();
+                if (diagram.hasNativeLayout()) {
+                    return true;
+                }
+                diagram.endLoad();
+                reloadDiagram();
+                return true;
+            }
+            // Close diagram entirely from topology
+            closeDiagram();
+            return true;
+        }
+        return false;
     }
 
     @Override
     public void navigateUp() {
-        routeTableState.selectPrevious();
+        if (!diagram.isShowDiagram()) {
+            routeTableState.selectPrevious();
+        }
     }
 
     @Override
     public void navigateDown() {
-        IntegrationInfo info = ctx.findSelectedIntegration();
-        routeTableState.selectNext(info != null ? info.routes.size() : 0);
+        if (!diagram.isShowDiagram()) {
+            IntegrationInfo info = ctx.findSelectedIntegration();
+            routeTableState.selectNext(info != null ? info.routes.size() : 0);
+        }
     }
 
     @Override
     public void onIntegrationChanged() {
         sourceViewer.reset();
         diagram.reset();
+        topologyMode = true;
+        drillDownRouteId = null;
+        routeNavigationStack.clear();
         routeTableState.select(0);
     }
 
@@ -232,36 +405,77 @@ class RoutesTab implements MonitorTab {
             return;
         }
 
-        // Fullscreen source view
-        if (sourceViewer.isVisible()) {
-            List<Rect> fullChunks = Layout.vertical()
-                    .constraints(Constraint.length(4), Constraint.fill())
-                    .split(area);
-            renderRouteHeader(frame, fullChunks.get(0), info);
-            sourceViewer.render(frame, fullChunks.get(1));
+        // Fullscreen source view (from table mode)
+        if (sourceViewer.isVisible() && !diagram.isShowDiagram()) {
+            sourceViewer.render(frame, area);
+            return;
+        }
+
+        // Fullscreen source view (from drill-down mode)
+        if (sourceViewer.isVisible() && diagram.isShowDiagram()) {
+            sourceViewer.render(frame, area);
             return;
         }
 
         // Fullscreen diagram mode
         if (diagram.isShowDiagram() && diagram.hasDiagramData()) {
-            String title = " Diagram [" + diagramRouteId + "] ";
-            if (diagramAllRoutes) {
-                diagram.renderDiagram(frame, area, title);
-            } else {
-                List<Rect> fullChunks = Layout.vertical()
-                        .constraints(Constraint.length(4), Constraint.fill())
-                        .split(area);
-                renderRouteHeader(frame, fullChunks.get(0), info);
-                diagram.renderDiagram(frame, fullChunks.get(1), title);
+            if (topologyMode && diagram.hasNativeLayout()) {
+                String selectedRouteId = diagram.getSelectedRouteId();
+                Line title;
+                if (info.name != null) {
+                    title = Line.from(
+                            Span.raw(" Topology ["),
+                            Span.styled(info.name, 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                            Span.raw("] "));
+                } else {
+                    title = Line.from(Span.raw(" Topology "));
+                }
+                if (selectedRouteId != null && area.width() > 60) {
+                    int panelWidth = 30;
+                    List<Rect> hChunks = Layout.horizontal()
+                            .constraints(Constraint.length(panelWidth), 
Constraint.fill())
+                            .split(area);
+                    renderInfoPanel(frame, hChunks.get(0), info, 
selectedRouteId);
+                    diagram.renderNativeDiagram(frame, hChunks.get(1), title, 
diagramMetrics);
+                } else {
+                    diagram.renderNativeDiagram(frame, area, title, 
diagramMetrics);
+                }
+                return;
+            } else if (!topologyMode && drillDownRouteId != null
+                    && diagram.getRouteLayout(drillDownRouteId) != null) {
+                Line title = buildBreadcrumbTitle();
+                var routeLayout = diagram.getRouteLayout(drillDownRouteId);
+                if (area.width() > 60) {
+                    int panelWidth = 30;
+                    List<Rect> hChunks = Layout.horizontal()
+                            .constraints(Constraint.length(panelWidth), 
Constraint.fill())
+                            .split(area);
+                    renderEipInfoPanel(frame, hChunks.get(0));
+                    diagram.renderNativeRouteDiagram(
+                            frame, hChunks.get(1), title, diagramMetrics, 
drillDownRouteId, routeLayout);
+                } else {
+                    diagram.renderNativeRouteDiagram(frame, area, title, 
diagramMetrics, drillDownRouteId, routeLayout);
+                }
+                return;
             }
+
+            // Fallback: loading or no native layout yet
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled(
+                                    "Loading diagram...",
+                                    Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Diagram ").build())
+                            .build(),
+                    area);
             return;
         }
 
-        // Sort routes
+        // Normal table view
         List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes);
         sortedRoutes.sort(this::sortRoute);
 
-        // Split: routes table (top half) + processors table (bottom half)
         List<Rect> chunks = Layout.vertical()
                 .constraints(Constraint.percentage(45), 
Constraint.percentage(55))
                 .split(area);
@@ -390,25 +604,20 @@ class RoutesTab implements MonitorTab {
 
         frame.renderStatefulWidget(routeTable, chunks.get(0), routeTableState);
 
-        // Bottom panel: diagram or processors
-        if (diagram.isShowDiagram() && diagram.hasDiagramData()) {
-            String title = " Diagram [" + diagramRouteId + "] ";
-            diagram.renderDiagram(frame, chunks.get(1), title);
+        // Bottom panel: processors
+        Integer selectedRoute = routeTableState.selected();
+        if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < 
sortedRoutes.size()) {
+            RouteInfo route = sortedRoutes.get(selectedRoute);
+            renderProcessors(frame, chunks.get(1), route);
+        } else if (!sortedRoutes.isEmpty()) {
+            renderProcessors(frame, chunks.get(1), sortedRoutes.get(0));
         } else {
-            Integer selectedRoute = routeTableState.selected();
-            if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < 
sortedRoutes.size()) {
-                RouteInfo route = sortedRoutes.get(selectedRoute);
-                renderProcessors(frame, chunks.get(1), route);
-            } else if (!sortedRoutes.isEmpty()) {
-                renderProcessors(frame, chunks.get(1), sortedRoutes.get(0));
-            } else {
-                frame.renderWidget(
-                        Paragraph.builder()
-                                .text(Text.from(Line.from(Span.styled("No 
routes", Style.EMPTY.dim()))))
-                                
.block(Block.builder().borderType(BorderType.ROUNDED).title(" Processors 
").build())
-                                .build(),
-                        chunks.get(1));
-            }
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("No routes", 
Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).title(" Processors 
").build())
+                            .build(),
+                    chunks.get(1));
         }
     }
 
@@ -417,9 +626,30 @@ class RoutesTab implements MonitorTab {
         if (sourceViewer.isVisible()) {
             sourceViewer.renderFooter(spans);
         } else if (diagram.isShowDiagram()) {
-            diagram.renderFooterHints(spans);
+            if (!topologyMode && !diagram.getEipNodeBoxes().isEmpty()) {
+                hint(spans, "Esc", "back");
+                hint(spans, "t", "topology");
+                hint(spans, "↑↓←→", "navigate");
+                hint(spans, "PgUp/PgDn", "page");
+                hint(spans, "c", "source");
+            } else if (!topologyMode) {
+                hint(spans, "Esc", "back");
+                hint(spans, "t", "topology");
+                hint(spans, "↑↓←→", "scroll");
+                hint(spans, "PgUp/PgDn", "page");
+            } else if (!diagram.getNodeBoxes().isEmpty()) {
+                hint(spans, "Esc", "close");
+                hint(spans, "↑↓←→", "navigate");
+                hint(spans, "Enter", "drill-down");
+                hint(spans, "PgUp/PgDn", "page");
+            } else {
+                diagram.renderFooterHints(spans);
+            }
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
-            hintLast(spans, "n", "description" + (diagram.isShowDescription() 
? " [on]" : " [off]"));
+            if (topologyMode) {
+                hint(spans, "e", "external" + (showExternal ? " [on]" : " 
[off]"));
+            }
+            hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         } else {
             hint(spans, "Esc", "back");
             hint(spans, "↑↓", "navigate");
@@ -429,7 +659,6 @@ class RoutesTab implements MonitorTab {
             if (!routeTopMode) {
                 hint(spans, "c", "source");
                 hint(spans, "d", "diagram");
-                hint(spans, "a", "diagram " + (diagramAllRoutes ? "[all]" : 
"[single]"));
                 String routeState = selectedRouteState();
                 boolean supSus = selectedRouteSupportsSuspension();
                 if ("Started".equals(routeState)) {
@@ -451,7 +680,7 @@ class RoutesTab implements MonitorTab {
 
     void refreshDiagramIfNeeded() {
         if (diagram.isShowDiagram() && diagramMetrics) {
-            reloadDiagramQuietly();
+            reloadDiagram();
         }
     }
 
@@ -462,6 +691,284 @@ class RoutesTab implements MonitorTab {
         return route.from != null ? route.from : "";
     }
 
+    // ---- Diagram open/close ----
+
+    private void openDiagram() {
+        topologyMode = true;
+        drillDownRouteId = null;
+        routeNavigationStack.clear();
+        diagram.setTopologyMode(true);
+
+        // Pre-select the currently highlighted route from the table
+        String selectedId = selectedRouteId();
+        if (selectedId != null) {
+            diagram.setPendingSelectionRouteId(selectedId);
+        }
+
+        loadDiagram(true);
+    }
+
+    private void closeDiagram() {
+        topologyMode = true;
+        drillDownRouteId = null;
+        routeNavigationStack.clear();
+        diagram.close();
+    }
+
+    // ---- Info panels (mirrored from DiagramTab) ----
+
+    private void renderInfoPanel(Frame frame, Rect area, IntegrationInfo info, 
String routeId) {
+        RouteInfo route = null;
+        for (RouteInfo r : info.routes) {
+            if (routeId.equals(r.routeId)) {
+                route = r;
+                break;
+            }
+        }
+
+        List<Line> lines = new ArrayList<>();
+        if (route != null) {
+            lines.add(Line.from(
+                    Span.styled(" Route: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.styled(route.routeId, 
Style.EMPTY.fg(Color.WHITE).bold())));
+            lines.add(Line.from(
+                    Span.styled(" From:  ", Style.EMPTY.dim()),
+                    Span.raw(route.from != null ? route.from : "")));
+            String stateLabel = route.state != null ? route.state : "";
+            Style stateStyle = "Started".equals(route.state) ? 
Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.LIGHT_RED);
+            lines.add(Line.from(
+                    Span.styled(" State: ", Style.EMPTY.dim()),
+                    Span.styled(stateLabel, stateStyle)));
+
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled(" Uptime:     ", Style.EMPTY.dim()),
+                    Span.raw(route.uptime != null ? route.uptime : "")));
+            lines.add(Line.from(
+                    Span.styled(" Throughput: ", Style.EMPTY.dim()),
+                    Span.raw(route.throughput != null ? route.throughput : 
"")));
+            if (route.coverage != null) {
+                lines.add(Line.from(
+                        Span.styled(" Coverage:   ", Style.EMPTY.dim()),
+                        Span.raw(route.coverage)));
+            }
+
+            lines.add(Line.from(Span.raw("")));
+            int w = numWidth(route.total, route.failed, route.inflight);
+            lines.add(Line.from(
+                    Span.styled(" Total:    ", Style.EMPTY.dim()),
+                    Span.raw(String.format("%" + w + "d", route.total))));
+            Style failStyle = route.failed > 0 ? 
Style.EMPTY.fg(Color.LIGHT_RED).bold() : Style.EMPTY;
+            lines.add(Line.from(
+                    Span.styled(" Failed:   ", Style.EMPTY.dim()),
+                    Span.styled(String.format("%" + w + "d", route.failed), 
failStyle)));
+            lines.add(Line.from(
+                    Span.styled(" Inflight: ", Style.EMPTY.dim()),
+                    Span.raw(String.format("%" + w + "d", route.inflight))));
+
+            lines.add(Line.from(Span.raw("")));
+            if (route.total > 0) {
+                int tw = numWidth(route.meanTime, route.maxTime, 
route.minTime);
+                lines.add(Line.from(
+                        Span.styled(" Mean: ", Style.EMPTY.dim()),
+                        Span.raw(String.format("%" + tw + "d ms", 
route.meanTime))));
+                lines.add(Line.from(
+                        Span.styled(" Max:  ", Style.EMPTY.dim()),
+                        Span.raw(String.format("%" + tw + "d ms", 
route.maxTime))));
+                lines.add(Line.from(
+                        Span.styled(" Min:  ", Style.EMPTY.dim()),
+                        Span.raw(String.format("%" + tw + "d ms", 
route.minTime))));
+            }
+
+            if (route.sinceLastCompleted != null || route.sinceLastFailed != 
null) {
+                lines.add(Line.from(Span.raw("")));
+                lines.add(Line.from(
+                        Span.styled(" Since last:", Style.EMPTY.dim())));
+                if (route.sinceLastCompleted != null) {
+                    lines.add(Line.from(
+                            Span.styled("   success: ", Style.EMPTY.dim()),
+                            Span.raw(route.sinceLastCompleted)));
+                }
+                if (route.sinceLastFailed != null) {
+                    lines.add(Line.from(
+                            Span.styled("   fail:    ", Style.EMPTY.dim()),
+                            Span.styled(route.sinceLastFailed,
+                                    Style.EMPTY.fg(Color.LIGHT_RED))));
+                }
+            }
+
+        } else {
+            var topoNode = diagram.getSelectedTopologyNode();
+            if (topoNode != null) {
+                boolean isInbound = "external-in".equals(topoNode.nodeType);
+                lines.add(Line.from(
+                        Span.styled(isInbound ? " Inbound" : " Outbound",
+                                Style.EMPTY.fg(Color.CYAN).bold())));
+                lines.add(Line.from(Span.raw("")));
+                lines.add(Line.from(
+                        Span.styled(" URI: ", Style.EMPTY.dim()),
+                        Span.raw(topoNode.from != null ? topoNode.from : "")));
+                if (topoNode.description != null && 
!topoNode.description.isBlank()) {
+                    lines.add(Line.from(
+                            Span.styled(" Path: ", Style.EMPTY.dim()),
+                            Span.raw(topoNode.description)));
+                }
+                String connectedRoute = diagram.getConnectedRouteId(routeId);
+                if (connectedRoute != null) {
+                    lines.add(Line.from(Span.raw("")));
+                    lines.add(Line.from(
+                            Span.styled(isInbound ? " To route: " : " From 
route: ", Style.EMPTY.dim()),
+                            Span.styled(connectedRoute, 
Style.EMPTY.fg(Color.WHITE))));
+                }
+                if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 
0) {
+                    lines.add(Line.from(Span.raw("")));
+                    lines.add(Line.from(
+                            Span.styled(" Total:  ", Style.EMPTY.dim()),
+                            
Span.raw(String.valueOf(topoNode.exchangesTotal))));
+                    if (topoNode.exchangesFailed > 0) {
+                        lines.add(Line.from(
+                                Span.styled(" Failed: ", Style.EMPTY.dim()),
+                                
Span.styled(String.valueOf(topoNode.exchangesFailed),
+                                        
Style.EMPTY.fg(Color.LIGHT_RED).bold())));
+                    }
+                }
+            } else {
+                lines.add(Line.from(
+                        Span.styled(" " + routeId, 
Style.EMPTY.fg(Color.CYAN).bold())));
+                lines.add(Line.from(
+                        Span.styled(" (external endpoint)", 
Style.EMPTY.dim())));
+            }
+        }
+
+        Paragraph paragraph = Paragraph.builder()
+                .text(Text.from(lines))
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Info ").build())
+                .build();
+        frame.renderWidget(paragraph, area);
+    }
+
+    private void renderEipInfoPanel(Frame frame, Rect area) {
+        List<Line> lines = new ArrayList<>();
+        var selected = diagram.getSelectedEipNodeBox();
+        if (selected != null && selected.layoutNode() != null) {
+            var ln = selected.layoutNode();
+
+            String typeLabel = ln.type != null ? ln.type : "unknown";
+            Color eipColor = 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors.getEipColor(typeLabel);
+            lines.add(Line.from(
+                    Span.styled(" [" + typeLabel + "]", 
Style.EMPTY.fg(eipColor).bold())));
+
+            String label = String.join("", ln.wrappedLines);
+            if (!label.isBlank()) {
+                lines.add(Line.from(
+                        Span.styled(" ", Style.EMPTY.dim()),
+                        Span.raw(label)));
+            }
+
+            if (ln.id != null) {
+                lines.add(Line.from(
+                        Span.styled(" ID: ", Style.EMPTY.dim()),
+                        Span.raw(ln.id)));
+            }
+
+            lines.add(Line.from(Span.raw("")));
+            String linkedRoute = diagram.findLinkedRouteId(drillDownRouteId);
+            if (linkedRoute != null && diagram.getRouteLayout(linkedRoute) != 
null) {
+                lines.add(Line.from(
+                        Span.styled(" ↵ ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                        Span.styled(linkedRoute, 
Style.EMPTY.fg(Color.WHITE))));
+            } else if (ln.treeNode != null && ln.treeNode.info.remote) {
+                String arrow = "from".equals(ln.type) ? " external → " : " → 
external";
+                lines.add(Line.from(
+                        Span.styled(arrow, Style.EMPTY.fg(Color.DARK_GRAY))));
+            } else {
+                lines.add(Line.from(Span.raw("")));
+            }
+
+            if (ln.treeNode != null && ln.treeNode.info.stat != null) {
+                var stat = ln.treeNode.info.stat;
+                lines.add(Line.from(Span.raw("")));
+                int w = numWidth(stat.exchangesTotal, stat.exchangesFailed, 
stat.exchangesInflight);
+                lines.add(Line.from(
+                        Span.styled(" Total:    ", Style.EMPTY.dim()),
+                        Span.raw(String.format("%" + w + "d", 
stat.exchangesTotal))));
+                Style failStyle = stat.exchangesFailed > 0
+                        ? Style.EMPTY.fg(Color.LIGHT_RED).bold() : Style.EMPTY;
+                lines.add(Line.from(
+                        Span.styled(" Failed:   ", Style.EMPTY.dim()),
+                        Span.styled(String.format("%" + w + "d", 
stat.exchangesFailed), failStyle)));
+                lines.add(Line.from(
+                        Span.styled(" Inflight: ", Style.EMPTY.dim()),
+                        Span.raw(String.format("%" + w + "d", 
stat.exchangesInflight))));
+
+                if (stat.exchangesTotal > 0) {
+                    lines.add(Line.from(Span.raw("")));
+                    int tw = numWidth(stat.meanProcessingTime, 
stat.maxProcessingTime,
+                            stat.minProcessingTime, stat.lastProcessingTime);
+                    lines.add(Line.from(
+                            Span.styled(" Mean: ", Style.EMPTY.dim()),
+                            Span.raw(String.format("%" + tw + "d ms", 
stat.meanProcessingTime))));
+                    lines.add(Line.from(
+                            Span.styled(" Max:  ", Style.EMPTY.dim()),
+                            Span.raw(String.format("%" + tw + "d ms", 
stat.maxProcessingTime))));
+                    lines.add(Line.from(
+                            Span.styled(" Min:  ", Style.EMPTY.dim()),
+                            Span.raw(String.format("%" + tw + "d ms", 
stat.minProcessingTime))));
+                    lines.add(Line.from(
+                            Span.styled(" Last: ", Style.EMPTY.dim()),
+                            Span.raw(String.format("%" + tw + "d ms", 
stat.lastProcessingTime))));
+
+                    if (stat.lastCompletedExchangeTimestamp > 0 || 
stat.lastFailedExchangeTimestamp > 0) {
+                        long now = System.currentTimeMillis();
+                        lines.add(Line.from(Span.raw("")));
+                        lines.add(Line.from(
+                                Span.styled(" Since last:", 
Style.EMPTY.dim())));
+                        if (stat.lastCompletedExchangeTimestamp > 0) {
+                            long ago = now - 
stat.lastCompletedExchangeTimestamp;
+                            lines.add(Line.from(
+                                    Span.styled("   success: ", 
Style.EMPTY.dim()),
+                                    Span.raw(TimeUtils.printDuration(ago, 
false))));
+                        }
+                        if (stat.lastFailedExchangeTimestamp > 0) {
+                            long ago = now - stat.lastFailedExchangeTimestamp;
+                            lines.add(Line.from(
+                                    Span.styled("   fail:    ", 
Style.EMPTY.dim()),
+                                    Span.styled(TimeUtils.printDuration(ago, 
false),
+                                            Style.EMPTY.fg(Color.LIGHT_RED))));
+                        }
+                    }
+                }
+            }
+        } else {
+            lines.add(Line.from(Span.styled(" (no node selected)", 
Style.EMPTY.dim())));
+        }
+
+        Paragraph paragraph = Paragraph.builder()
+                .text(Text.from(lines))
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Info ").build())
+                .build();
+        frame.renderWidget(paragraph, area);
+    }
+
+    private Line buildBreadcrumbTitle() {
+        Style nameStyle = Style.EMPTY.fg(Color.YELLOW).bold();
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" Route ["));
+        if (routeNavigationStack.isEmpty()) {
+            spans.add(Span.styled(drillDownRouteId, nameStyle));
+        } else {
+            for (var it = routeNavigationStack.descendingIterator(); 
it.hasNext();) {
+                spans.add(Span.styled(it.next(), nameStyle));
+                spans.add(Span.raw(" → "));
+            }
+            spans.add(Span.styled(drillDownRouteId, nameStyle));
+        }
+        spans.add(Span.raw("] "));
+        return Line.from(spans);
+    }
+
     // ---- Rendering helpers ----
 
     private void renderProcessors(Frame frame, Rect area, RouteInfo route) {
@@ -606,64 +1113,6 @@ class RoutesTab implements MonitorTab {
         frame.renderStatefulWidget(table, area, processorTableState);
     }
 
-    private void renderRouteHeader(Frame frame, Rect area, IntegrationInfo 
info) {
-        RouteInfo route = null;
-        if (diagramRouteId != null) {
-            for (RouteInfo r : info.routes) {
-                if (diagramRouteId.equals(r.routeId)) {
-                    route = r;
-                    break;
-                }
-            }
-        }
-
-        List<Row> rows = new ArrayList<>();
-        if (route != null) {
-            Style stateStyle = "Started".equals(route.state)
-                    ? Style.EMPTY.fg(Color.GREEN)
-                    : Style.EMPTY.fg(Color.LIGHT_RED);
-            Style failStyle = route.failed > 0
-                    ? Style.EMPTY.fg(Color.LIGHT_RED).bold()
-                    : Style.EMPTY;
-            rows.add(Row.from(
-                    Cell.from(Span.styled(route.routeId != null ? 
route.routeId : "", Style.EMPTY.fg(Color.CYAN))),
-                    Cell.from(routeFromLabel(route)),
-                    Cell.from(Span.styled(route.state != null ? route.state : 
"", stateStyle)),
-                    Cell.from(route.uptime != null ? route.uptime : ""),
-                    rightCell(route.throughput != null ? route.throughput : 
"", 8),
-                    rightCell(String.valueOf(route.total), 8),
-                    rightCell(String.valueOf(route.failed), 6, failStyle),
-                    rightCell(String.valueOf(route.inflight), 8)));
-        }
-
-        Table table = Table.builder()
-                .rows(rows)
-                .header(Row.from(
-                        Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
-                        Cell.from(Span.styled("FROM", Style.EMPTY.bold())),
-                        Cell.from(Span.styled("STATUS", Style.EMPTY.bold())),
-                        Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
-                        rightCell("MSG/S", 8, Style.EMPTY.bold()),
-                        rightCell("TOTAL", 8, Style.EMPTY.bold()),
-                        rightCell("FAIL", 6, Style.EMPTY.bold()),
-                        rightCell("INFLIGHT", 8, Style.EMPTY.bold())))
-                .widths(
-                        Constraint.length(12),
-                        Constraint.fill(),
-                        Constraint.length(10),
-                        Constraint.length(8),
-                        Constraint.length(8),
-                        Constraint.length(8),
-                        Constraint.length(6),
-                        Constraint.length(8))
-                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
-                .block(Block.builder().borderType(BorderType.ROUNDED)
-                        .title(" Route ").build())
-                .build();
-
-        frame.renderStatefulWidget(table, area, routeHeaderTableState);
-    }
-
     // ---- Sorting ----
 
     private int sortRoute(RouteInfo a, RouteInfo b) {
@@ -831,15 +1280,7 @@ class RoutesTab implements MonitorTab {
 
     // ---- Async loading ----
 
-    private void loadDiagramForSelectedRoute() {
-        loadDiagramForSelectedRoute(true);
-    }
-
-    private void reloadDiagramQuietly() {
-        loadDiagramForSelectedRoute(false);
-    }
-
-    private void loadDiagramForSelectedRoute(boolean showPlaceholder) {
+    private void loadDiagram(boolean showPlaceholder) {
         if (ctx.selectedPid == null || ctx.runner == null) {
             return;
         }
@@ -847,41 +1288,28 @@ class RoutesTab implements MonitorTab {
             return;
         }
 
-        IntegrationInfo info = ctx.findSelectedIntegration();
-        if (info == null || info.routes.isEmpty()) {
-            diagram.endLoad();
-            return;
-        }
-
-        List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes);
-        sortedRoutes.sort(this::sortRoute);
-
-        Integer sel = routeTableState.selected();
-        RouteInfo selectedRoute;
-        if (sel != null && sel >= 0 && sel < sortedRoutes.size()) {
-            selectedRoute = sortedRoutes.get(sel);
-        } else {
-            selectedRoute = sortedRoutes.get(0);
-        }
-
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
-        String routeId = diagramAllRoutes ? null : selectedRoute.routeId;
+        boolean external = showExternal;
 
-        diagramRouteId = routeId != null ? routeId : "all";
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
         }
 
         ctx.runner.scheduler().execute(() -> {
             try {
-                diagram.loadRouteDiagramInBackground(ctx, pid, routeId, 
showMetrics);
+                diagram.setTopologyMode(topologyMode);
+                diagram.loadAllDiagramsInBackground(ctx, pid, showMetrics, 
external);
             } finally {
                 diagram.endLoad();
             }
         });
     }
 
+    private void reloadDiagram() {
+        loadDiagram(false);
+    }
+
     private void loadSourceForSelectedRoute() {
         IntegrationInfo info = ctx.findSelectedIntegration();
         if (info == null || info.routes.isEmpty()) {
@@ -895,6 +1323,19 @@ class RoutesTab implements MonitorTab {
         sourceViewer.loadSource(ctx, selectedRoute.routeId, 0);
     }
 
+    private void loadSourceForSelectedNode() {
+        if (drillDownRouteId == null) {
+            return;
+        }
+        int targetLine = 0;
+        var selected = diagram.getSelectedEipNodeBox();
+        if (selected != null && selected.layoutNode() != null
+                && selected.layoutNode().treeNode != null) {
+            targetLine = selected.layoutNode().treeNode.info.line;
+        }
+        sourceViewer.loadSource(ctx, drillDownRouteId, targetLine);
+    }
+
     @Override
     public SelectionContext getSelectionContext() {
         IntegrationInfo info = ctx.findSelectedIntegration();
@@ -951,51 +1392,68 @@ class RoutesTab implements MonitorTab {
 
                 ## Route Diagram
 
-                Press `d` to see a visual flow chart of the selected route. 
The diagram
-                shows every EIP node and how messages flow between them. 
Numbers on
-                each node show how many exchanges passed through it.
+                Press `d` to see a topology diagram showing how all routes 
connect to each
+                other. This is the same view as the Diagram tab. Use arrow 
keys to navigate
+                between route boxes and press `Enter` to drill down into a 
route's internal
+                EIP structure.
 
-                Scroll down to view the diagram example:
+                ## Navigation
 
-                ```
-                    ┌──────────────────────┐
-                    │ from[timer:hello?..] │
-                    └──────────────────────┘
-                                │
-                                ▼ 29
-                    ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                    ╎  ┌────────────────────────┐  ╎
-                    ╎  │        choice          │  ╎
-                    ╎  └────────────────────────┘  ╎
-                    ╎       │              │       ╎
-                    ╎       ▼ 9            ▼ 20    ╎
-                    ╎  ┌──────────┐  ┌──────────┐  ╎
-                    ╎  │ log[HI]  │  │ log[LO]  │  ╎
-                    ╎  └──────────┘  └──────────┘  ╎
-                    ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                ```
+                In the topology view, use arrow keys to select route boxes:
+                - `↑↓` moves between layers (upstream/downstream routes)
+                - `←→` moves between routes in the same layer
+
+                When a route is selected, an **Info panel** appears on the left
+                showing key metrics: state, uptime, throughput, exchange 
counts,
+                and processing times.
+
+                Press `Enter` on a selected route to **drill down** into its
+                internal EIP structure (the route diagram). Press `Esc` to
+                return to the topology view.
 
-                The dotted border groups nodes that belong to the same EIP 
block
-                (like `choice`, `split`, `multicast`). The numbers show how 
many
-                exchanges took each branch — useful for verifying routing 
logic.
+                ## Route Diagram (drill-down)
+
+                In the route diagram, each EIP node shows its type tag 
(colored)
+                and endpoint URI or description. Nodes that connect to other 
routes
+                display a `↵` indicator — press `Enter` to jump directly to the
+                linked route's diagram.
+
+                Navigation history is maintained as a stack: pressing `Esc` 
goes
+                back to the previous route, and eventually back to the 
topology view.
 
                 ## Source View
 
-                Press `s` to see the original route source code (YAML, XML, or 
Java).
+                Press `c` to see the original route source code (YAML, XML, or 
Java).
 
                 ## Keys
 
+                **Route table:**
                 - `Up/Down` — select route
                 - `p` — start/stop selected route
                 - `P` — suspend/resume selected route
-                - `d` — show route diagram
-                - `a` — toggle diagram scope (single route or all routes)
+                - `d` — show topology diagram
                 - `c` — show route source code
-                - `m` — toggle metrics in diagram
+                - `n` — toggle description labels
                 - `s` — cycle sort column
                 - `S` — reverse sort order
                 - `t` — toggle Top mode
-                - `Esc` — back to route list
+
+                **Topology view:**
+                - `↑↓←→` — navigate between route boxes
+                - `Enter` — drill down into selected route
+                - `Esc` — close diagram (back to route table)
+                - `m` — toggle metrics on/off
+                - `e` — toggle external systems on/off
+                - `n` — toggle description labels
+
+                **Route diagram (drill-down):**
+                - `↑↓←→` — navigate between EIP nodes
+                - `Enter` — jump to linked route (when `↵` indicator shown)
+                - `Esc` — go back (previous route or topology)
+                - `t` — jump back to topology view
+                - `c` — show source code
+                - `m` — toggle metrics
+                - `n` — toggle description labels
                 """;
     }
 
@@ -1033,4 +1491,12 @@ class RoutesTab implements MonitorTab {
         result.put("selectedIndex", sel != null ? sel : -1);
         return result;
     }
+
+    private static int numWidth(long... values) {
+        long max = 0;
+        for (long v : values) {
+            max = Math.max(max, Math.abs(v));
+        }
+        return Math.max(1, String.valueOf(max).length());
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index cbfc5e124b25..3e4f6b0ae5af 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -378,7 +378,7 @@ class RunOptionsForm {
         int popupW = Math.min(56, area.width() - 4);
         int popupH = 11;
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
 
         frame.renderWidget(Clear.INSTANCE, popup);
@@ -448,7 +448,7 @@ class RunOptionsForm {
         int propCount = properties != null ? properties.size() : 0;
         int popupH = Math.min(propCount + 2, Math.min(20, area.height() - 4));
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
 
         frame.renderWidget(Clear.INSTANCE, popup);

Reply via email to