detached fad762b

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 {
7070 focusMgr *focus.Manager
7171 borderMgr *focus.BorderManager
7272
73+ resizeModeBorderTarget layout.Widget
74+
7375 // Omnibox configuration (omnibox is created per workspace view)
7476 omniboxCfg component.OmniboxConfig
7577 // 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) {
910912 log := logging.FromContext(ctx)
911913 log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed")
912914
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.
914924 if a.borderMgr != nil {
915925 a.borderMgr.OnModeChange(ctx, from, to)
916926 }
917927 }
918928
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+
919970 // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area.
920971 func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) {
921972 a.createWorkspaceViewWithoutAttach(ctx, tab)
@@ -961,14 +1012,24 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
9611012 a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView)
9621013 }
9631014
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.
9661017
9671018 // Set omnibox config for this workspace view
9681019 wsView.SetOmniboxConfig(a.omniboxCfg)
9691020 // Set find bar config for this workspace view
9701021 wsView.SetFindBarConfig(a.findBarCfg)
9711022
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+
9721033 // Store in map
9731034 a.workspaceViews[tab.ID] = wsView
9741035
@@ -70,6 +70,8 @@ type App struct {
70 focusMgr *focus.Manager70 focusMgr *focus.Manager
71 borderMgr *focus.BorderManager71 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.OmniboxConfig76 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 mode915+ 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-workspace1015+ // Note: Mode borders for tab/pane/session are attached to MainWindow.
965- // This ensures it's visible regardless of which tab is active1016+ // Resize mode border is attached to the active pane's stack container.
966 1017
967 // Set omnibox config for this workspace view1018 // 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 view1020 // 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 map1033 // Store in map
973 a.workspaceViews[tab.ID] = wsView1034 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)
7676 bm.borderOverlay.SetVisible(true)
7777 case input.ModeNormal:
7878 bm.borderOverlay.SetVisible(false)
79+ default:
80+ // Resize mode is handled by per-pane stack border, not global overlay.
81+ bm.borderOverlay.SetVisible(false)
7982 }
8083
8184 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 = to84 bm.currentMode = to
M internal/ui/theme/css.go +10 -0
@@ -475,6 +475,16 @@ func generatePaneCSS(p Palette) string {
475475 box-shadow: inset 0 0 0 0.25em #FFA500;
476476 border-radius: 0;
477477 }
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+}
478488 `
479489 }
480490
@@ -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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close