feat(ui): add resize mode border and styling
- commit
- fad762b36d8b0c0b01cc9c7d30842fefea63841c
- author
- brice · · 2025-12-26 07:04 UTC
- parent
- c9cbc6e
- signature
- unsigned
3 files · +77 -3
M internal/ui/app.go +64 -3
| @@ -70,6 +70,8 @@ type App struct { | |||
| 70 | 70 | focusMgr *focus.Manager | |
| 71 | 71 | borderMgr *focus.BorderManager | |
| 72 | 72 | ||
| 73 | + | resizeModeBorderTarget layout.Widget | |
| 74 | + | ||
| 73 | 75 | // Omnibox configuration (omnibox is created per workspace view) | |
| 74 | 76 | omniboxCfg component.OmniboxConfig | |
| 75 | 77 | // Find bar configuration (find bar is created per workspace view) | |
| @@ -910,12 +912,61 @@ func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) { | |||
| 910 | 912 | log := logging.FromContext(ctx) | |
| 911 | 913 | log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed") | |
| 912 | 914 | ||
| 913 | - | // Update border overlay visibility based on mode | |
| 915 | + | if from == input.ModeResize && to != input.ModeResize { | |
| 916 | + | a.clearResizeModeBorder() | |
| 917 | + | } | |
| 918 | + | if to == input.ModeResize { | |
| 919 | + | a.applyResizeModeBorder(ctx, a.activeWorkspace()) | |
| 920 | + | } | |
| 921 | + | ||
| 922 | + | // Update global border overlay visibility based on mode. | |
| 923 | + | // Note: resize mode border is handled per-pane (stack container), not via global overlay. | |
| 914 | 924 | if a.borderMgr != nil { | |
| 915 | 925 | a.borderMgr.OnModeChange(ctx, from, to) | |
| 916 | 926 | } | |
| 917 | 927 | } | |
| 918 | 928 | ||
| 929 | + | func (a *App) clearResizeModeBorder() { | |
| 930 | + | if a.resizeModeBorderTarget != nil { | |
| 931 | + | a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active") | |
| 932 | + | a.resizeModeBorderTarget = nil | |
| 933 | + | } | |
| 934 | + | } | |
| 935 | + | ||
| 936 | + | func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) { | |
| 937 | + | wsView := a.activeWorkspaceView() | |
| 938 | + | if wsView == nil || ws == nil { | |
| 939 | + | a.clearResizeModeBorder() | |
| 940 | + | return | |
| 941 | + | } | |
| 942 | + | ||
| 943 | + | paneID := ws.ActivePaneID | |
| 944 | + | if paneID == "" { | |
| 945 | + | a.clearResizeModeBorder() | |
| 946 | + | return | |
| 947 | + | } | |
| 948 | + | ||
| 949 | + | // Important: wrap the whole stack container when the active pane is in a stack. | |
| 950 | + | target := wsView.GetStackContainerWidget(paneID) | |
| 951 | + | if target == nil { | |
| 952 | + | a.clearResizeModeBorder() | |
| 953 | + | return | |
| 954 | + | } | |
| 955 | + | ||
| 956 | + | if a.resizeModeBorderTarget != nil && a.resizeModeBorderTarget != target { | |
| 957 | + | a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active") | |
| 958 | + | } | |
| 959 | + | if !target.HasCssClass("resize-mode-active") { | |
| 960 | + | target.AddCssClass("resize-mode-active") | |
| 961 | + | } | |
| 962 | + | if a.resizeModeBorderTarget == nil { | |
| 963 | + | logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached") | |
| 964 | + | } | |
| 965 | + | if a.resizeModeBorderTarget != target { | |
| 966 | + | a.resizeModeBorderTarget = target | |
| 967 | + | } | |
| 968 | + | } | |
| 969 | + | ||
| 919 | 970 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | |
| 920 | 971 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | |
| 921 | 972 | a.createWorkspaceViewWithoutAttach(ctx, tab) | |
| @@ -961,14 +1012,24 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity. | |||
| 961 | 1012 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | |
| 962 | 1013 | } | |
| 963 | 1014 | ||
| 964 | - | // Note: Border overlay is attached to MainWindow, not per-workspace | |
| 965 | - | // This ensures it's visible regardless of which tab is active | |
| 1015 | + | // Note: Mode borders for tab/pane/session are attached to MainWindow. | |
| 1016 | + | // Resize mode border is attached to the active pane's stack container. | |
| 966 | 1017 | ||
| 967 | 1018 | // Set omnibox config for this workspace view | |
| 968 | 1019 | wsView.SetOmniboxConfig(a.omniboxCfg) | |
| 969 | 1020 | // Set find bar config for this workspace view | |
| 970 | 1021 | wsView.SetFindBarConfig(a.findBarCfg) | |
| 971 | 1022 | ||
| 1023 | + | wsView.SetOnPaneFocused(func(paneID entity.PaneID) { | |
| 1024 | + | if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize { | |
| 1025 | + | ws := a.activeWorkspace() | |
| 1026 | + | if ws != nil { | |
| 1027 | + | ws.ActivePaneID = paneID | |
| 1028 | + | } | |
| 1029 | + | a.applyResizeModeBorder(ctx, ws) | |
| 1030 | + | } | |
| 1031 | + | }) | |
| 1032 | + | ||
| 972 | 1033 | // Store in map | |
| 973 | 1034 | a.workspaceViews[tab.ID] = wsView | |
| 974 | 1035 | ||
| @@ -70,6 +70,8 @@ type App struct { | |||||
| 70 | focusMgr *focus.Manager | 70 | focusMgr *focus.Manager | ||
| 71 | borderMgr *focus.BorderManager | 71 | borderMgr *focus.BorderManager | ||
| 72 | 72 | ||||
| 73 | + | resizeModeBorderTarget layout.Widget | |||
| 74 | + | ||||
| 73 | // Omnibox configuration (omnibox is created per workspace view) | 75 | // Omnibox configuration (omnibox is created per workspace view) | ||
| 74 | omniboxCfg component.OmniboxConfig | 76 | omniboxCfg component.OmniboxConfig | ||
| 75 | // Find bar configuration (find bar is created per workspace view) | 77 | // Find bar configuration (find bar is created per workspace view) | ||
| @@ -910,12 +912,61 @@ func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) { | |||||
| 910 | log := logging.FromContext(ctx) | 912 | log := logging.FromContext(ctx) | ||
| 911 | log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed") | 913 | log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed") | ||
| 912 | 914 | ||||
| 913 | - | // Update border overlay visibility based on mode | 915 | + | if from == input.ModeResize && to != input.ModeResize { |
| 916 | + | a.clearResizeModeBorder() | |||
| 917 | + | } | |||
| 918 | + | if to == input.ModeResize { | |||
| 919 | + | a.applyResizeModeBorder(ctx, a.activeWorkspace()) | |||
| 920 | + | } | |||
| 921 | + | ||||
| 922 | + | // Update global border overlay visibility based on mode. | |||
| 923 | + | // Note: resize mode border is handled per-pane (stack container), not via global overlay. | |||
| 914 | if a.borderMgr != nil { | 924 | if a.borderMgr != nil { | ||
| 915 | a.borderMgr.OnModeChange(ctx, from, to) | 925 | a.borderMgr.OnModeChange(ctx, from, to) | ||
| 916 | } | 926 | } | ||
| 917 | } | 927 | } | ||
| 918 | 928 | ||||
| 929 | + | func (a *App) clearResizeModeBorder() { | |||
| 930 | + | if a.resizeModeBorderTarget != nil { | |||
| 931 | + | a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active") | |||
| 932 | + | a.resizeModeBorderTarget = nil | |||
| 933 | + | } | |||
| 934 | + | } | |||
| 935 | + | ||||
| 936 | + | func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) { | |||
| 937 | + | wsView := a.activeWorkspaceView() | |||
| 938 | + | if wsView == nil || ws == nil { | |||
| 939 | + | a.clearResizeModeBorder() | |||
| 940 | + | return | |||
| 941 | + | } | |||
| 942 | + | ||||
| 943 | + | paneID := ws.ActivePaneID | |||
| 944 | + | if paneID == "" { | |||
| 945 | + | a.clearResizeModeBorder() | |||
| 946 | + | return | |||
| 947 | + | } | |||
| 948 | + | ||||
| 949 | + | // Important: wrap the whole stack container when the active pane is in a stack. | |||
| 950 | + | target := wsView.GetStackContainerWidget(paneID) | |||
| 951 | + | if target == nil { | |||
| 952 | + | a.clearResizeModeBorder() | |||
| 953 | + | return | |||
| 954 | + | } | |||
| 955 | + | ||||
| 956 | + | if a.resizeModeBorderTarget != nil && a.resizeModeBorderTarget != target { | |||
| 957 | + | a.resizeModeBorderTarget.RemoveCssClass("resize-mode-active") | |||
| 958 | + | } | |||
| 959 | + | if !target.HasCssClass("resize-mode-active") { | |||
| 960 | + | target.AddCssClass("resize-mode-active") | |||
| 961 | + | } | |||
| 962 | + | if a.resizeModeBorderTarget == nil { | |||
| 963 | + | logging.FromContext(ctx).Debug().Str("pane_id", string(paneID)).Msg("resize mode border attached") | |||
| 964 | + | } | |||
| 965 | + | if a.resizeModeBorderTarget != target { | |||
| 966 | + | a.resizeModeBorderTarget = target | |||
| 967 | + | } | |||
| 968 | + | } | |||
| 969 | + | ||||
| 919 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | 970 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | ||
| 920 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | 971 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | ||
| 921 | a.createWorkspaceViewWithoutAttach(ctx, tab) | 972 | a.createWorkspaceViewWithoutAttach(ctx, tab) | ||
| @@ -961,14 +1012,24 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity. | |||||
| 961 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | 1012 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | ||
| 962 | } | 1013 | } | ||
| 963 | 1014 | ||||
| 964 | - | // Note: Border overlay is attached to MainWindow, not per-workspace | 1015 | + | // Note: Mode borders for tab/pane/session are attached to MainWindow. |
| 965 | - | // This ensures it's visible regardless of which tab is active | 1016 | + | // Resize mode border is attached to the active pane's stack container. |
| 966 | 1017 | ||||
| 967 | // Set omnibox config for this workspace view | 1018 | // Set omnibox config for this workspace view | ||
| 968 | wsView.SetOmniboxConfig(a.omniboxCfg) | 1019 | wsView.SetOmniboxConfig(a.omniboxCfg) | ||
| 969 | // Set find bar config for this workspace view | 1020 | // Set find bar config for this workspace view | ||
| 970 | wsView.SetFindBarConfig(a.findBarCfg) | 1021 | wsView.SetFindBarConfig(a.findBarCfg) | ||
| 971 | 1022 | ||||
| 1023 | + | wsView.SetOnPaneFocused(func(paneID entity.PaneID) { | |||
| 1024 | + | if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize { | |||
| 1025 | + | ws := a.activeWorkspace() | |||
| 1026 | + | if ws != nil { | |||
| 1027 | + | ws.ActivePaneID = paneID | |||
| 1028 | + | } | |||
| 1029 | + | a.applyResizeModeBorder(ctx, ws) | |||
| 1030 | + | } | |||
| 1031 | + | }) | |||
| 1032 | + | ||||
| 972 | // Store in map | 1033 | // Store in map | ||
| 973 | a.workspaceViews[tab.ID] = wsView | 1034 | a.workspaceViews[tab.ID] = wsView | ||
| 974 | 1035 | ||||
M internal/ui/focus/borders.go +3 -0
| @@ -76,6 +76,9 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode) | |||
| 76 | 76 | bm.borderOverlay.SetVisible(true) | |
| 77 | 77 | case input.ModeNormal: | |
| 78 | 78 | bm.borderOverlay.SetVisible(false) | |
| 79 | + | default: | |
| 80 | + | // Resize mode is handled by per-pane stack border, not global overlay. | |
| 81 | + | bm.borderOverlay.SetVisible(false) | |
| 79 | 82 | } | |
| 80 | 83 | ||
| 81 | 84 | bm.currentMode = to | |
| @@ -76,6 +76,9 @@ func (bm *BorderManager) OnModeChange(ctx context.Context, from, to input.Mode) | |||||
| 76 | bm.borderOverlay.SetVisible(true) | 76 | bm.borderOverlay.SetVisible(true) | ||
| 77 | case input.ModeNormal: | 77 | case input.ModeNormal: | ||
| 78 | bm.borderOverlay.SetVisible(false) | 78 | bm.borderOverlay.SetVisible(false) | ||
| 79 | + | default: | |||
| 80 | + | // Resize mode is handled by per-pane stack border, not global overlay. | |||
| 81 | + | bm.borderOverlay.SetVisible(false) | |||
| 79 | } | 82 | } | ||
| 80 | 83 | ||||
| 81 | bm.currentMode = to | 84 | bm.currentMode = to | ||
M internal/ui/theme/css.go +10 -0
| @@ -475,6 +475,16 @@ func generatePaneCSS(p Palette) string { | |||
| 475 | 475 | box-shadow: inset 0 0 0 0.25em #FFA500; | |
| 476 | 476 | border-radius: 0; | |
| 477 | 477 | } | |
| 478 | + | ||
| 479 | + | /* Resize mode active - thick teal inset border */ | |
| 480 | + | .resize-mode-active { | |
| 481 | + | background-color: transparent; | |
| 482 | + | /* Prefer inset shadow, but also set outline for widgets that don't paint shadows */ | |
| 483 | + | box-shadow: inset 0 0 0 0.25em #00D4AA; | |
| 484 | + | outline: 0.25em solid #00D4AA; | |
| 485 | + | outline-offset: -0.25em; | |
| 486 | + | border-radius: 0; | |
| 487 | + | } | |
| 478 | 488 | ` | |
| 479 | 489 | } | |
| 480 | 490 | ||
| @@ -475,6 +475,16 @@ func generatePaneCSS(p Palette) string { | |||||
| 475 | box-shadow: inset 0 0 0 0.25em #FFA500; | 475 | box-shadow: inset 0 0 0 0.25em #FFA500; | ||
| 476 | border-radius: 0; | 476 | border-radius: 0; | ||
| 477 | } | 477 | } | ||
| 478 | + | ||||
| 479 | + | /* Resize mode active - thick teal inset border */ | |||
| 480 | + | .resize-mode-active { | |||
| 481 | + | background-color: transparent; | |||
| 482 | + | /* Prefer inset shadow, but also set outline for widgets that don't paint shadows */ | |||
| 483 | + | box-shadow: inset 0 0 0 0.25em #00D4AA; | |||
| 484 | + | outline: 0.25em solid #00D4AA; | |||
| 485 | + | outline-offset: -0.25em; | |||
| 486 | + | border-radius: 0; | |||
| 487 | + | } | |||
| 478 | ` | 488 | ` | ||
| 479 | } | 489 | } | ||
| 480 | 490 | ||||