feat(usecase): persist split pane ratios on manual resize
- commit
- 19bb708202f82adbf85336c264de640856e4d911
- author
- brice · · 2025-12-26 07:37 UTC
- parent
- 917b52c
- signature
- unsigned
3 files · +84 -0
M internal/application/usecase/manage_panes.go +49 -0
| @@ -260,6 +260,55 @@ func (uc *ManagePanesUseCase) Resize( | |||
| 260 | 260 | return nil | |
| 261 | 261 | } | |
| 262 | 262 | ||
| 263 | + | type SetSplitRatioInput struct { | |
| 264 | + | Workspace *entity.Workspace | |
| 265 | + | SplitNodeID string | |
| 266 | + | Ratio float64 | |
| 267 | + | MinPanePercent float64 | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | func (uc *ManagePanesUseCase) SetSplitRatio(ctx context.Context, input SetSplitRatioInput) error { | |
| 271 | + | log := logging.FromContext(ctx) | |
| 272 | + | if uc == nil { | |
| 273 | + | return fmt.Errorf("manage panes use case is nil") | |
| 274 | + | } | |
| 275 | + | if input.Workspace == nil { | |
| 276 | + | return fmt.Errorf("workspace is required") | |
| 277 | + | } | |
| 278 | + | if input.Workspace.Root == nil { | |
| 279 | + | return ErrNothingToResize | |
| 280 | + | } | |
| 281 | + | if input.SplitNodeID == "" { | |
| 282 | + | return fmt.Errorf("split node id is required") | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | var splitNode *entity.PaneNode | |
| 286 | + | input.Workspace.Root.Walk(func(node *entity.PaneNode) bool { | |
| 287 | + | if node.ID == input.SplitNodeID { | |
| 288 | + | splitNode = node | |
| 289 | + | return false | |
| 290 | + | } | |
| 291 | + | return true | |
| 292 | + | }) | |
| 293 | + | ||
| 294 | + | if splitNode == nil || !splitNode.IsSplit() { | |
| 295 | + | return fmt.Errorf("split node not found: %s", input.SplitNodeID) | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | minRatio := input.MinPanePercent / 100.0 | |
| 299 | + | maxRatio := 1.0 - minRatio | |
| 300 | + | oldRatio := splitNode.SplitRatio | |
| 301 | + | splitNode.SplitRatio = clampFloat64(input.Ratio, minRatio, maxRatio) | |
| 302 | + | ||
| 303 | + | log.Debug(). | |
| 304 | + | Str("split_node_id", input.SplitNodeID). | |
| 305 | + | Float64("old_ratio", oldRatio). | |
| 306 | + | Float64("new_ratio", splitNode.SplitRatio). | |
| 307 | + | Msg("split ratio set") | |
| 308 | + | ||
| 309 | + | return nil | |
| 310 | + | } | |
| 311 | + | ||
| 263 | 312 | func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection { | |
| 264 | 313 | splitNode, axis, isStartChild := findNearestSplitForResize(target) | |
| 265 | 314 | if splitNode == nil { | |
| @@ -260,6 +260,55 @@ func (uc *ManagePanesUseCase) Resize( | |||||
| 260 | return nil | 260 | return nil | ||
| 261 | } | 261 | } | ||
| 262 | 262 | ||||
| 263 | + | type SetSplitRatioInput struct { | |||
| 264 | + | Workspace *entity.Workspace | |||
| 265 | + | SplitNodeID string | |||
| 266 | + | Ratio float64 | |||
| 267 | + | MinPanePercent float64 | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | func (uc *ManagePanesUseCase) SetSplitRatio(ctx context.Context, input SetSplitRatioInput) error { | |||
| 271 | + | log := logging.FromContext(ctx) | |||
| 272 | + | if uc == nil { | |||
| 273 | + | return fmt.Errorf("manage panes use case is nil") | |||
| 274 | + | } | |||
| 275 | + | if input.Workspace == nil { | |||
| 276 | + | return fmt.Errorf("workspace is required") | |||
| 277 | + | } | |||
| 278 | + | if input.Workspace.Root == nil { | |||
| 279 | + | return ErrNothingToResize | |||
| 280 | + | } | |||
| 281 | + | if input.SplitNodeID == "" { | |||
| 282 | + | return fmt.Errorf("split node id is required") | |||
| 283 | + | } | |||
| 284 | + | ||||
| 285 | + | var splitNode *entity.PaneNode | |||
| 286 | + | input.Workspace.Root.Walk(func(node *entity.PaneNode) bool { | |||
| 287 | + | if node.ID == input.SplitNodeID { | |||
| 288 | + | splitNode = node | |||
| 289 | + | return false | |||
| 290 | + | } | |||
| 291 | + | return true | |||
| 292 | + | }) | |||
| 293 | + | ||||
| 294 | + | if splitNode == nil || !splitNode.IsSplit() { | |||
| 295 | + | return fmt.Errorf("split node not found: %s", input.SplitNodeID) | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | minRatio := input.MinPanePercent / 100.0 | |||
| 299 | + | maxRatio := 1.0 - minRatio | |||
| 300 | + | oldRatio := splitNode.SplitRatio | |||
| 301 | + | splitNode.SplitRatio = clampFloat64(input.Ratio, minRatio, maxRatio) | |||
| 302 | + | ||||
| 303 | + | log.Debug(). | |||
| 304 | + | Str("split_node_id", input.SplitNodeID). | |||
| 305 | + | Float64("old_ratio", oldRatio). | |||
| 306 | + | Float64("new_ratio", splitNode.SplitRatio). | |||
| 307 | + | Msg("split ratio set") | |||
| 308 | + | ||||
| 309 | + | return nil | |||
| 310 | + | } | |||
| 311 | + | ||||
| 263 | func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection { | 312 | func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection { | ||
| 264 | splitNode, axis, isStartChild := findNearestSplitForResize(target) | 313 | splitNode, axis, isStartChild := findNearestSplitForResize(target) | ||
| 265 | if splitNode == nil { | 314 | if splitNode == nil { | ||
M internal/ui/app.go +6 -0
| @@ -1030,6 +1030,12 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity. | |||
| 1030 | 1030 | } | |
| 1031 | 1031 | }) | |
| 1032 | 1032 | ||
| 1033 | + | wsView.SetOnSplitRatioDragged(func(nodeID string, ratio float64) { | |
| 1034 | + | if a.wsCoord != nil { | |
| 1035 | + | _ = a.wsCoord.SetSplitRatio(ctx, nodeID, ratio) | |
| 1036 | + | } | |
| 1037 | + | }) | |
| 1038 | + | ||
| 1033 | 1039 | // Store in map | |
| 1034 | 1040 | a.workspaceViews[tab.ID] = wsView | |
| 1035 | 1041 | ||
| @@ -1030,6 +1030,12 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity. | |||||
| 1030 | } | 1030 | } | ||
| 1031 | }) | 1031 | }) | ||
| 1032 | 1032 | ||||
| 1033 | + | wsView.SetOnSplitRatioDragged(func(nodeID string, ratio float64) { | |||
| 1034 | + | if a.wsCoord != nil { | |||
| 1035 | + | _ = a.wsCoord.SetSplitRatio(ctx, nodeID, ratio) | |||
| 1036 | + | } | |||
| 1037 | + | }) | |||
| 1038 | + | ||||
| 1033 | // Store in map | 1039 | // Store in map | ||
| 1034 | a.workspaceViews[tab.ID] = wsView | 1040 | a.workspaceViews[tab.ID] = wsView | ||
| 1035 | 1041 | ||||
M internal/ui/coordinator/workspace.go +29 -0
| @@ -1900,6 +1900,35 @@ func (c *WorkspaceCoordinator) Resize(ctx context.Context, dir usecase.ResizeDir | |||
| 1900 | 1900 | return nil | |
| 1901 | 1901 | } | |
| 1902 | 1902 | ||
| 1903 | + | func (c *WorkspaceCoordinator) SetSplitRatio(ctx context.Context, splitNodeID string, ratio float64) error { | |
| 1904 | + | log := logging.FromContext(ctx) | |
| 1905 | + | ||
| 1906 | + | if c.panesUC == nil { | |
| 1907 | + | log.Warn().Msg("panes use case not available") | |
| 1908 | + | return nil | |
| 1909 | + | } | |
| 1910 | + | ||
| 1911 | + | ws, _ := c.getActiveWS() | |
| 1912 | + | if ws == nil { | |
| 1913 | + | log.Warn().Msg("no active workspace") | |
| 1914 | + | return nil | |
| 1915 | + | } | |
| 1916 | + | ||
| 1917 | + | cfg := config.Get() | |
| 1918 | + | err := c.panesUC.SetSplitRatio(ctx, usecase.SetSplitRatioInput{ | |
| 1919 | + | Workspace: ws, | |
| 1920 | + | SplitNodeID: splitNodeID, | |
| 1921 | + | Ratio: ratio, | |
| 1922 | + | MinPanePercent: cfg.Workspace.ResizeMode.MinPanePercent, | |
| 1923 | + | }) | |
| 1924 | + | if err != nil { | |
| 1925 | + | return err | |
| 1926 | + | } | |
| 1927 | + | ||
| 1928 | + | c.notifyStateChanged() | |
| 1929 | + | return nil | |
| 1930 | + | } | |
| 1931 | + | ||
| 1903 | 1932 | func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) { | |
| 1904 | 1933 | if wsView == nil || ws == nil || ws.Root == nil { | |
| 1905 | 1934 | return | |
| @@ -1900,6 +1900,35 @@ func (c *WorkspaceCoordinator) Resize(ctx context.Context, dir usecase.ResizeDir | |||||
| 1900 | return nil | 1900 | return nil | ||
| 1901 | } | 1901 | } | ||
| 1902 | 1902 | ||||
| 1903 | + | func (c *WorkspaceCoordinator) SetSplitRatio(ctx context.Context, splitNodeID string, ratio float64) error { | |||
| 1904 | + | log := logging.FromContext(ctx) | |||
| 1905 | + | ||||
| 1906 | + | if c.panesUC == nil { | |||
| 1907 | + | log.Warn().Msg("panes use case not available") | |||
| 1908 | + | return nil | |||
| 1909 | + | } | |||
| 1910 | + | ||||
| 1911 | + | ws, _ := c.getActiveWS() | |||
| 1912 | + | if ws == nil { | |||
| 1913 | + | log.Warn().Msg("no active workspace") | |||
| 1914 | + | return nil | |||
| 1915 | + | } | |||
| 1916 | + | ||||
| 1917 | + | cfg := config.Get() | |||
| 1918 | + | err := c.panesUC.SetSplitRatio(ctx, usecase.SetSplitRatioInput{ | |||
| 1919 | + | Workspace: ws, | |||
| 1920 | + | SplitNodeID: splitNodeID, | |||
| 1921 | + | Ratio: ratio, | |||
| 1922 | + | MinPanePercent: cfg.Workspace.ResizeMode.MinPanePercent, | |||
| 1923 | + | }) | |||
| 1924 | + | if err != nil { | |||
| 1925 | + | return err | |||
| 1926 | + | } | |||
| 1927 | + | ||||
| 1928 | + | c.notifyStateChanged() | |||
| 1929 | + | return nil | |||
| 1930 | + | } | |||
| 1931 | + | ||||
| 1903 | func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) { | 1932 | func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) { | ||
| 1904 | if wsView == nil || ws == nil || ws.Root == nil { | 1933 | if wsView == nil || ws == nil || ws.Root == nil { | ||
| 1905 | return | 1934 | return | ||