fix(ui): wire split ratio persistence for incremental splits and suppress early notify
- commit
- c4aef32781653904d26ef5d7e4cdaff2614bfb8d
- author
- brice · · 2025-12-26 08:01 UTC
- parent
- 67bb8aa
- signature
- unsigned
2 files · +18 -1
M internal/ui/coordinator/workspace.go +16 -0
| @@ -323,6 +323,7 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||
| 323 | 323 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 324 | 324 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |
| 325 | 325 | } | |
| 326 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |
| 326 | 327 | ||
| 327 | 328 | wsView.SetRootWidgetDirect(splitView.Widget()) | |
| 328 | 329 | ||
| @@ -370,6 +371,7 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||
| 370 | 371 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 371 | 372 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |
| 372 | 373 | } | |
| 374 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |
| 373 | 375 | ||
| 374 | 376 | // Put the new split view in the grandparent's slot | |
| 375 | 377 | if isStartChild { | |
| @@ -562,6 +564,18 @@ func splitOrientation(direction usecase.SplitDirection) (layout.Orientation, boo | |||
| 562 | 564 | } | |
| 563 | 565 | } | |
| 564 | 566 | ||
| 567 | + | func (c *WorkspaceCoordinator) wireSplitRatioPersistence(ctx context.Context, splitView *layout.SplitView, splitNodeID string) { | |
| 568 | + | if c == nil || splitView == nil || splitNodeID == "" { | |
| 569 | + | return | |
| 570 | + | } | |
| 571 | + | ||
| 572 | + | splitView.SetOnRatioChanged(func(ratio float64) { | |
| 573 | + | if err := c.SetSplitRatio(ctx, splitNodeID, ratio); err != nil { | |
| 574 | + | logging.FromContext(ctx).Warn().Err(err).Str("split_node_id", splitNodeID).Msg("failed to persist split ratio") | |
| 575 | + | } | |
| 576 | + | }) | |
| 577 | + | } | |
| 578 | + | ||
| 565 | 579 | func (c *WorkspaceCoordinator) replaceRootSplit( | |
| 566 | 580 | ctx context.Context, | |
| 567 | 581 | wsView *component.WorkspaceView, | |
| @@ -584,6 +598,7 @@ func (c *WorkspaceCoordinator) replaceRootSplit( | |||
| 584 | 598 | splitView = layout.NewSplitView(ctx, factory, orientation, | |
| 585 | 599 | newStackedView.Widget(), existingRootWidget, output.SplitRatio) | |
| 586 | 600 | } | |
| 601 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |
| 587 | 602 | ||
| 588 | 603 | wsView.SetRootWidgetDirect(splitView.Widget()) | |
| 589 | 604 | tr.RegisterSplit(output.ParentNode.ID, splitView.Widget(), orientation) | |
| @@ -643,6 +658,7 @@ func (c *WorkspaceCoordinator) replaceNonRootSplit( | |||
| 643 | 658 | splitView = layout.NewSplitView(ctx, factory, orientation, | |
| 644 | 659 | newStackedView.Widget(), activePaneWidget, output.SplitRatio) | |
| 645 | 660 | } | |
| 661 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |
| 646 | 662 | ||
| 647 | 663 | if isStartChild { | |
| 648 | 664 | panedWidget.SetStartChild(splitView.Widget()) | |
| @@ -323,6 +323,7 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||||
| 323 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | 323 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | ||
| 324 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | 324 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | ||
| 325 | } | 325 | } | ||
| 326 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |||
| 326 | 327 | ||||
| 327 | wsView.SetRootWidgetDirect(splitView.Widget()) | 328 | wsView.SetRootWidgetDirect(splitView.Widget()) | ||
| 328 | 329 | ||||
| @@ -370,6 +371,7 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||||
| 370 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | 371 | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | ||
| 371 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | 372 | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | ||
| 372 | } | 373 | } | ||
| 374 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |||
| 373 | 375 | ||||
| 374 | // Put the new split view in the grandparent's slot | 376 | // Put the new split view in the grandparent's slot | ||
| 375 | if isStartChild { | 377 | if isStartChild { | ||
| @@ -562,6 +564,18 @@ func splitOrientation(direction usecase.SplitDirection) (layout.Orientation, boo | |||||
| 562 | } | 564 | } | ||
| 563 | } | 565 | } | ||
| 564 | 566 | ||||
| 567 | + | func (c *WorkspaceCoordinator) wireSplitRatioPersistence(ctx context.Context, splitView *layout.SplitView, splitNodeID string) { | |||
| 568 | + | if c == nil || splitView == nil || splitNodeID == "" { | |||
| 569 | + | return | |||
| 570 | + | } | |||
| 571 | + | ||||
| 572 | + | splitView.SetOnRatioChanged(func(ratio float64) { | |||
| 573 | + | if err := c.SetSplitRatio(ctx, splitNodeID, ratio); err != nil { | |||
| 574 | + | logging.FromContext(ctx).Warn().Err(err).Str("split_node_id", splitNodeID).Msg("failed to persist split ratio") | |||
| 575 | + | } | |||
| 576 | + | }) | |||
| 577 | + | } | |||
| 578 | + | ||||
| 565 | func (c *WorkspaceCoordinator) replaceRootSplit( | 579 | func (c *WorkspaceCoordinator) replaceRootSplit( | ||
| 566 | ctx context.Context, | 580 | ctx context.Context, | ||
| 567 | wsView *component.WorkspaceView, | 581 | wsView *component.WorkspaceView, | ||
| @@ -584,6 +598,7 @@ func (c *WorkspaceCoordinator) replaceRootSplit( | |||||
| 584 | splitView = layout.NewSplitView(ctx, factory, orientation, | 598 | splitView = layout.NewSplitView(ctx, factory, orientation, | ||
| 585 | newStackedView.Widget(), existingRootWidget, output.SplitRatio) | 599 | newStackedView.Widget(), existingRootWidget, output.SplitRatio) | ||
| 586 | } | 600 | } | ||
| 601 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |||
| 587 | 602 | ||||
| 588 | wsView.SetRootWidgetDirect(splitView.Widget()) | 603 | wsView.SetRootWidgetDirect(splitView.Widget()) | ||
| 589 | tr.RegisterSplit(output.ParentNode.ID, splitView.Widget(), orientation) | 604 | tr.RegisterSplit(output.ParentNode.ID, splitView.Widget(), orientation) | ||
| @@ -643,6 +658,7 @@ func (c *WorkspaceCoordinator) replaceNonRootSplit( | |||||
| 643 | splitView = layout.NewSplitView(ctx, factory, orientation, | 658 | splitView = layout.NewSplitView(ctx, factory, orientation, | ||
| 644 | newStackedView.Widget(), activePaneWidget, output.SplitRatio) | 659 | newStackedView.Widget(), activePaneWidget, output.SplitRatio) | ||
| 645 | } | 660 | } | ||
| 661 | + | c.wireSplitRatioPersistence(ctx, splitView, output.ParentNode.ID) | |||
| 646 | 662 | ||||
| 647 | if isStartChild { | 663 | if isStartChild { | ||
| 648 | panedWidget.SetStartChild(splitView.Widget()) | 664 | panedWidget.SetStartChild(splitView.Widget()) | ||
M internal/ui/layout/split.go +2 -1
| @@ -35,6 +35,7 @@ type SplitView struct { | |||
| 35 | 35 | const maxRetryFrames = 120 | |
| 36 | 36 | ||
| 37 | 37 | const notifyPositionDebounceDelay = 100 * time.Millisecond | |
| 38 | + | const notifyPositionSuppressAfterApplyDelay = 50 * time.Millisecond | |
| 38 | 39 | ||
| 39 | 40 | // NewSplitView creates a new split view with the given orientation and children. | |
| 40 | 41 | // The ratio determines the initial divider position (0.0-1.0). | |
| @@ -226,7 +227,7 @@ func (sv *SplitView) ApplyRatio() bool { | |||
| 226 | 227 | // Avoid treating programmatic SetPosition as a user resize. | |
| 227 | 228 | // We suppress notify::position briefly; GTK may emit during/after SetPosition. | |
| 228 | 229 | sv.mu.Lock() | |
| 229 | - | sv.suppressNotifyUntil = time.Now().Add(50 * time.Millisecond) | |
| 230 | + | sv.suppressNotifyUntil = time.Now().Add(notifyPositionSuppressAfterApplyDelay) | |
| 230 | 231 | sv.mu.Unlock() | |
| 231 | 232 | ||
| 232 | 233 | sv.paned.SetPosition(position) | |
| @@ -35,6 +35,7 @@ type SplitView struct { | |||||
| 35 | const maxRetryFrames = 120 | 35 | const maxRetryFrames = 120 | ||
| 36 | 36 | ||||
| 37 | const notifyPositionDebounceDelay = 100 * time.Millisecond | 37 | const notifyPositionDebounceDelay = 100 * time.Millisecond | ||
| 38 | + | const notifyPositionSuppressAfterApplyDelay = 50 * time.Millisecond | |||
| 38 | 39 | ||||
| 39 | // NewSplitView creates a new split view with the given orientation and children. | 40 | // NewSplitView creates a new split view with the given orientation and children. | ||
| 40 | // The ratio determines the initial divider position (0.0-1.0). | 41 | // The ratio determines the initial divider position (0.0-1.0). | ||
| @@ -226,7 +227,7 @@ func (sv *SplitView) ApplyRatio() bool { | |||||
| 226 | // Avoid treating programmatic SetPosition as a user resize. | 227 | // Avoid treating programmatic SetPosition as a user resize. | ||
| 227 | // We suppress notify::position briefly; GTK may emit during/after SetPosition. | 228 | // We suppress notify::position briefly; GTK may emit during/after SetPosition. | ||
| 228 | sv.mu.Lock() | 229 | sv.mu.Lock() | ||
| 229 | - | sv.suppressNotifyUntil = time.Now().Add(50 * time.Millisecond) | 230 | + | sv.suppressNotifyUntil = time.Now().Add(notifyPositionSuppressAfterApplyDelay) |
| 230 | sv.mu.Unlock() | 231 | sv.mu.Unlock() | ||
| 231 | 232 | ||||
| 232 | sv.paned.SetPosition(position) | 233 | sv.paned.SetPosition(position) | ||