feat(ui): detect and propagate split pane resize events
- commit
- 917b52c54753e357321bf8033aff0948954c7ff5
- author
- brice · · 2025-12-26 07:36 UTC
- parent
- dc9d28d
- signature
- unsigned
9 files · +161 -2
M internal/ui/component/workspace_view.go +17 -1
| @@ -47,7 +47,8 @@ type WorkspaceView struct { | |||
| 47 | 47 | workspace *entity.Workspace | |
| 48 | 48 | paneViews map[entity.PaneID]*PaneView | |
| 49 | 49 | ||
| 50 | - | onPaneFocused func(paneID entity.PaneID) | |
| 50 | + | onPaneFocused func(paneID entity.PaneID) | |
| 51 | + | onSplitRatioDragged func(nodeID string, ratio float64) | |
| 51 | 52 | ||
| 52 | 53 | mu sync.RWMutex | |
| 53 | 54 | } | |
| @@ -128,6 +129,14 @@ func NewWorkspaceView(ctx context.Context, factory layout.WidgetFactory) *Worksp | |||
| 128 | 129 | ||
| 129 | 130 | // Create tree renderer with our adapter as the pane view factory | |
| 130 | 131 | wv.treeRenderer = layout.NewTreeRenderer(ctx, factory, &paneViewFactoryAdapter{wv: wv, ctx: ctx}) | |
| 132 | + | wv.treeRenderer.SetOnSplitRatioChanged(func(nodeID string, ratio float64) { | |
| 133 | + | wv.mu.RLock() | |
| 134 | + | cb := wv.onSplitRatioDragged | |
| 135 | + | wv.mu.RUnlock() | |
| 136 | + | if cb != nil { | |
| 137 | + | cb(nodeID, ratio) | |
| 138 | + | } | |
| 139 | + | }) | |
| 131 | 140 | ||
| 132 | 141 | return wv | |
| 133 | 142 | } | |
| @@ -303,6 +312,13 @@ func (wv *WorkspaceView) SetOnPaneFocused(fn func(paneID entity.PaneID)) { | |||
| 303 | 312 | wv.onPaneFocused = fn | |
| 304 | 313 | } | |
| 305 | 314 | ||
| 315 | + | func (wv *WorkspaceView) SetOnSplitRatioDragged(fn func(nodeID string, ratio float64)) { | |
| 316 | + | wv.mu.Lock() | |
| 317 | + | defer wv.mu.Unlock() | |
| 318 | + | ||
| 319 | + | wv.onSplitRatioDragged = fn | |
| 320 | + | } | |
| 321 | + | ||
| 306 | 322 | // Rebuild rebuilds the widget tree from the current workspace. | |
| 307 | 323 | // Use this after structural changes like splits or closes. | |
| 308 | 324 | func (wv *WorkspaceView) Rebuild(ctx context.Context) error { | |
| @@ -47,7 +47,8 @@ type WorkspaceView struct { | |||||
| 47 | workspace *entity.Workspace | 47 | workspace *entity.Workspace | ||
| 48 | paneViews map[entity.PaneID]*PaneView | 48 | paneViews map[entity.PaneID]*PaneView | ||
| 49 | 49 | ||||
| 50 | - | onPaneFocused func(paneID entity.PaneID) | 50 | + | onPaneFocused func(paneID entity.PaneID) |
| 51 | + | onSplitRatioDragged func(nodeID string, ratio float64) | |||
| 51 | 52 | ||||
| 52 | mu sync.RWMutex | 53 | mu sync.RWMutex | ||
| 53 | } | 54 | } | ||
| @@ -128,6 +129,14 @@ func NewWorkspaceView(ctx context.Context, factory layout.WidgetFactory) *Worksp | |||||
| 128 | 129 | ||||
| 129 | // Create tree renderer with our adapter as the pane view factory | 130 | // Create tree renderer with our adapter as the pane view factory | ||
| 130 | wv.treeRenderer = layout.NewTreeRenderer(ctx, factory, &paneViewFactoryAdapter{wv: wv, ctx: ctx}) | 131 | wv.treeRenderer = layout.NewTreeRenderer(ctx, factory, &paneViewFactoryAdapter{wv: wv, ctx: ctx}) | ||
| 132 | + | wv.treeRenderer.SetOnSplitRatioChanged(func(nodeID string, ratio float64) { | |||
| 133 | + | wv.mu.RLock() | |||
| 134 | + | cb := wv.onSplitRatioDragged | |||
| 135 | + | wv.mu.RUnlock() | |||
| 136 | + | if cb != nil { | |||
| 137 | + | cb(nodeID, ratio) | |||
| 138 | + | } | |||
| 139 | + | }) | |||
| 131 | 140 | ||||
| 132 | return wv | 141 | return wv | ||
| 133 | } | 142 | } | ||
| @@ -303,6 +312,13 @@ func (wv *WorkspaceView) SetOnPaneFocused(fn func(paneID entity.PaneID)) { | |||||
| 303 | wv.onPaneFocused = fn | 312 | wv.onPaneFocused = fn | ||
| 304 | } | 313 | } | ||
| 305 | 314 | ||||
| 315 | + | func (wv *WorkspaceView) SetOnSplitRatioDragged(fn func(nodeID string, ratio float64)) { | |||
| 316 | + | wv.mu.Lock() | |||
| 317 | + | defer wv.mu.Unlock() | |||
| 318 | + | ||||
| 319 | + | wv.onSplitRatioDragged = fn | |||
| 320 | + | } | |||
| 321 | + | ||||
| 306 | // Rebuild rebuilds the widget tree from the current workspace. | 322 | // Rebuild rebuilds the widget tree from the current workspace. | ||
| 307 | // Use this after structural changes like splits or closes. | 323 | // Use this after structural changes like splits or closes. | ||
| 308 | func (wv *WorkspaceView) Rebuild(ctx context.Context) error { | 324 | func (wv *WorkspaceView) Rebuild(ctx context.Context) error { | ||
M internal/ui/component/workspace_view_test.go +1 -0
| @@ -199,6 +199,7 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||
| 199 | 199 | mockStackBox2.EXPECT().SetVisible(true).Once() | |
| 200 | 200 | mockPaned.EXPECT().SetStartChild(mockStackBox1).Once() | |
| 201 | 201 | mockPaned.EXPECT().SetEndChild(mockStackBox2).Once() | |
| 202 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 202 | 203 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 203 | 204 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 204 | 205 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -199,6 +199,7 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||||
| 199 | mockStackBox2.EXPECT().SetVisible(true).Once() | 199 | mockStackBox2.EXPECT().SetVisible(true).Once() | ||
| 200 | mockPaned.EXPECT().SetStartChild(mockStackBox1).Once() | 200 | mockPaned.EXPECT().SetStartChild(mockStackBox1).Once() | ||
| 201 | mockPaned.EXPECT().SetEndChild(mockStackBox2).Once() | 201 | mockPaned.EXPECT().SetEndChild(mockStackBox2).Once() | ||
| 202 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 202 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | 203 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | ||
| 203 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 204 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 204 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 205 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
M internal/ui/layout/gtk_widgets.go +9 -0
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package layout | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/jwijenbergh/purego" | |
| 4 | 5 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 6 | + | "github.com/jwijenbergh/puregotk/v4/gobject" | |
| 5 | 7 | "github.com/jwijenbergh/puregotk/v4/graphene" | |
| 6 | 8 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 7 | 9 | "github.com/jwijenbergh/puregotk/v4/pango" | |
| @@ -162,6 +164,13 @@ func (p *gtkPaned) ConnectMap(callback func()) uint32 { | |||
| 162 | 164 | return p.inner.ConnectMap(&cb) | |
| 163 | 165 | } | |
| 164 | 166 | ||
| 167 | + | func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint32 { | |
| 168 | + | cbPtr := purego.NewCallback(func() { | |
| 169 | + | callback() | |
| 170 | + | }) | |
| 171 | + | return gobject.SignalConnect(p.inner.GoPointer(), "notify::position", cbPtr) | |
| 172 | + | } | |
| 173 | + | ||
| 165 | 174 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | |
| 166 | 175 | cb := gtk.TickCallback(func(widget uintptr, frameClock uintptr, userData uintptr) bool { | |
| 167 | 176 | return callback() | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package layout | 1 | package layout | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/jwijenbergh/purego" | |||
| 4 | "github.com/jwijenbergh/puregotk/v4/gdk" | 5 | "github.com/jwijenbergh/puregotk/v4/gdk" | ||
| 6 | + | "github.com/jwijenbergh/puregotk/v4/gobject" | |||
| 5 | "github.com/jwijenbergh/puregotk/v4/graphene" | 7 | "github.com/jwijenbergh/puregotk/v4/graphene" | ||
| 6 | "github.com/jwijenbergh/puregotk/v4/gtk" | 8 | "github.com/jwijenbergh/puregotk/v4/gtk" | ||
| 7 | "github.com/jwijenbergh/puregotk/v4/pango" | 9 | "github.com/jwijenbergh/puregotk/v4/pango" | ||
| @@ -162,6 +164,13 @@ func (p *gtkPaned) ConnectMap(callback func()) uint32 { | |||||
| 162 | return p.inner.ConnectMap(&cb) | 164 | return p.inner.ConnectMap(&cb) | ||
| 163 | } | 165 | } | ||
| 164 | 166 | ||||
| 167 | + | func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint32 { | |||
| 168 | + | cbPtr := purego.NewCallback(func() { | |||
| 169 | + | callback() | |||
| 170 | + | }) | |||
| 171 | + | return gobject.SignalConnect(p.inner.GoPointer(), "notify::position", cbPtr) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 165 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | 174 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | ||
| 166 | cb := gtk.TickCallback(func(widget uintptr, frameClock uintptr, userData uintptr) bool { | 175 | cb := gtk.TickCallback(func(widget uintptr, frameClock uintptr, userData uintptr) bool { | ||
| 167 | return callback() | 176 | return callback() | ||
M internal/ui/layout/mocks/mocks.go +51 -0
| @@ -1495,6 +1495,57 @@ func (_c *MockPanedWidget_ConnectMap_Call) RunAndReturn(run func(callback func() | |||
| 1495 | 1495 | return _c | |
| 1496 | 1496 | } | |
| 1497 | 1497 | ||
| 1498 | + | // ConnectNotifyPosition provides a mock function for the type MockPanedWidget | |
| 1499 | + | func (_mock *MockPanedWidget) ConnectNotifyPosition(callback func()) uint32 { | |
| 1500 | + | ret := _mock.Called(callback) | |
| 1501 | + | ||
| 1502 | + | if len(ret) == 0 { | |
| 1503 | + | panic("no return value specified for ConnectNotifyPosition") | |
| 1504 | + | } | |
| 1505 | + | ||
| 1506 | + | var r0 uint32 | |
| 1507 | + | if returnFunc, ok := ret.Get(0).(func(func()) uint32); ok { | |
| 1508 | + | r0 = returnFunc(callback) | |
| 1509 | + | } else { | |
| 1510 | + | r0 = ret.Get(0).(uint32) | |
| 1511 | + | } | |
| 1512 | + | return r0 | |
| 1513 | + | } | |
| 1514 | + | ||
| 1515 | + | // MockPanedWidget_ConnectNotifyPosition_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectNotifyPosition' | |
| 1516 | + | type MockPanedWidget_ConnectNotifyPosition_Call struct { | |
| 1517 | + | *mock.Call | |
| 1518 | + | } | |
| 1519 | + | ||
| 1520 | + | // ConnectNotifyPosition is a helper method to define mock.On call | |
| 1521 | + | // - callback func() | |
| 1522 | + | func (_e *MockPanedWidget_Expecter) ConnectNotifyPosition(callback interface{}) *MockPanedWidget_ConnectNotifyPosition_Call { | |
| 1523 | + | return &MockPanedWidget_ConnectNotifyPosition_Call{Call: _e.mock.On("ConnectNotifyPosition", callback)} | |
| 1524 | + | } | |
| 1525 | + | ||
| 1526 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) Run(run func(callback func())) *MockPanedWidget_ConnectNotifyPosition_Call { | |
| 1527 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1528 | + | var arg0 func() | |
| 1529 | + | if args[0] != nil { | |
| 1530 | + | arg0 = args[0].(func()) | |
| 1531 | + | } | |
| 1532 | + | run( | |
| 1533 | + | arg0, | |
| 1534 | + | ) | |
| 1535 | + | }) | |
| 1536 | + | return _c | |
| 1537 | + | } | |
| 1538 | + | ||
| 1539 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) Return(v uint32) *MockPanedWidget_ConnectNotifyPosition_Call { | |
| 1540 | + | _c.Call.Return(v) | |
| 1541 | + | return _c | |
| 1542 | + | } | |
| 1543 | + | ||
| 1544 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) RunAndReturn(run func(callback func()) uint32) *MockPanedWidget_ConnectNotifyPosition_Call { | |
| 1545 | + | _c.Call.Return(run) | |
| 1546 | + | return _c | |
| 1547 | + | } | |
| 1548 | + | ||
| 1498 | 1549 | // GetAllocatedHeight provides a mock function for the type MockPanedWidget | |
| 1499 | 1550 | func (_mock *MockPanedWidget) GetAllocatedHeight() int { | |
| 1500 | 1551 | ret := _mock.Called() | |
| @@ -1495,6 +1495,57 @@ func (_c *MockPanedWidget_ConnectMap_Call) RunAndReturn(run func(callback func() | |||||
| 1495 | return _c | 1495 | return _c | ||
| 1496 | } | 1496 | } | ||
| 1497 | 1497 | ||||
| 1498 | + | // ConnectNotifyPosition provides a mock function for the type MockPanedWidget | |||
| 1499 | + | func (_mock *MockPanedWidget) ConnectNotifyPosition(callback func()) uint32 { | |||
| 1500 | + | ret := _mock.Called(callback) | |||
| 1501 | + | ||||
| 1502 | + | if len(ret) == 0 { | |||
| 1503 | + | panic("no return value specified for ConnectNotifyPosition") | |||
| 1504 | + | } | |||
| 1505 | + | ||||
| 1506 | + | var r0 uint32 | |||
| 1507 | + | if returnFunc, ok := ret.Get(0).(func(func()) uint32); ok { | |||
| 1508 | + | r0 = returnFunc(callback) | |||
| 1509 | + | } else { | |||
| 1510 | + | r0 = ret.Get(0).(uint32) | |||
| 1511 | + | } | |||
| 1512 | + | return r0 | |||
| 1513 | + | } | |||
| 1514 | + | ||||
| 1515 | + | // MockPanedWidget_ConnectNotifyPosition_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectNotifyPosition' | |||
| 1516 | + | type MockPanedWidget_ConnectNotifyPosition_Call struct { | |||
| 1517 | + | *mock.Call | |||
| 1518 | + | } | |||
| 1519 | + | ||||
| 1520 | + | // ConnectNotifyPosition is a helper method to define mock.On call | |||
| 1521 | + | // - callback func() | |||
| 1522 | + | func (_e *MockPanedWidget_Expecter) ConnectNotifyPosition(callback interface{}) *MockPanedWidget_ConnectNotifyPosition_Call { | |||
| 1523 | + | return &MockPanedWidget_ConnectNotifyPosition_Call{Call: _e.mock.On("ConnectNotifyPosition", callback)} | |||
| 1524 | + | } | |||
| 1525 | + | ||||
| 1526 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) Run(run func(callback func())) *MockPanedWidget_ConnectNotifyPosition_Call { | |||
| 1527 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 1528 | + | var arg0 func() | |||
| 1529 | + | if args[0] != nil { | |||
| 1530 | + | arg0 = args[0].(func()) | |||
| 1531 | + | } | |||
| 1532 | + | run( | |||
| 1533 | + | arg0, | |||
| 1534 | + | ) | |||
| 1535 | + | }) | |||
| 1536 | + | return _c | |||
| 1537 | + | } | |||
| 1538 | + | ||||
| 1539 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) Return(v uint32) *MockPanedWidget_ConnectNotifyPosition_Call { | |||
| 1540 | + | _c.Call.Return(v) | |||
| 1541 | + | return _c | |||
| 1542 | + | } | |||
| 1543 | + | ||||
| 1544 | + | func (_c *MockPanedWidget_ConnectNotifyPosition_Call) RunAndReturn(run func(callback func()) uint32) *MockPanedWidget_ConnectNotifyPosition_Call { | |||
| 1545 | + | _c.Call.Return(run) | |||
| 1546 | + | return _c | |||
| 1547 | + | } | |||
| 1548 | + | ||||
| 1498 | // GetAllocatedHeight provides a mock function for the type MockPanedWidget | 1549 | // GetAllocatedHeight provides a mock function for the type MockPanedWidget | ||
| 1499 | func (_mock *MockPanedWidget) GetAllocatedHeight() int { | 1550 | func (_mock *MockPanedWidget) GetAllocatedHeight() int { | ||
| 1500 | ret := _mock.Called() | 1551 | ret := _mock.Called() | ||
M internal/ui/layout/split.go +54 -0
| @@ -3,8 +3,10 @@ package layout | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "sync" | |
| 6 | + | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/logging" | |
| 9 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 8 | 10 | "github.com/rs/zerolog" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| @@ -18,6 +20,10 @@ type SplitView struct { | |||
| 18 | 20 | ratio float64 // 0.0-1.0, relative position of divider | |
| 19 | 21 | logger zerolog.Logger | |
| 20 | 22 | ||
| 23 | + | onRatioChanged func(ratio float64) | |
| 24 | + | pendingNotifyRatio float64 | |
| 25 | + | notifyDebounceTimer *time.Timer | |
| 26 | + | ||
| 21 | 27 | mu sync.RWMutex | |
| 22 | 28 | } | |
| 23 | 29 | ||
| @@ -25,6 +31,8 @@ type SplitView struct { | |||
| 25 | 31 | // At 60fps, 120 frames = ~2 seconds timeout. | |
| 26 | 32 | const maxRetryFrames = 120 | |
| 27 | 33 | ||
| 34 | + | const notifyPositionDebounceDelay = 100 * time.Millisecond | |
| 35 | + | ||
| 28 | 36 | // NewSplitView creates a new split view with the given orientation and children. | |
| 29 | 37 | // The ratio determines the initial divider position (0.0-1.0). | |
| 30 | 38 | func NewSplitView( | |
| @@ -61,6 +69,45 @@ func NewSplitView( | |||
| 61 | 69 | paned.SetEndChild(endChild) | |
| 62 | 70 | } | |
| 63 | 71 | ||
| 72 | + | paned.ConnectNotifyPosition(func() { | |
| 73 | + | position := paned.GetPosition() | |
| 74 | + | var totalSize int | |
| 75 | + | if orientation == OrientationHorizontal { | |
| 76 | + | totalSize = paned.GetAllocatedWidth() | |
| 77 | + | } else { | |
| 78 | + | totalSize = paned.GetAllocatedHeight() | |
| 79 | + | } | |
| 80 | + | if totalSize <= 0 { | |
| 81 | + | return | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | ratio := clampRatio(float64(position) / float64(totalSize)) | |
| 85 | + | ||
| 86 | + | sv.mu.Lock() | |
| 87 | + | sv.ratio = ratio | |
| 88 | + | sv.pendingNotifyRatio = ratio | |
| 89 | + | onRatioChanged := sv.onRatioChanged | |
| 90 | + | ||
| 91 | + | if sv.notifyDebounceTimer != nil { | |
| 92 | + | sv.notifyDebounceTimer.Stop() | |
| 93 | + | } | |
| 94 | + | sv.notifyDebounceTimer = time.AfterFunc(notifyPositionDebounceDelay, func() { | |
| 95 | + | if onRatioChanged == nil { | |
| 96 | + | return | |
| 97 | + | } | |
| 98 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 99 | + | sv.mu.RLock() | |
| 100 | + | pending := sv.pendingNotifyRatio | |
| 101 | + | sv.mu.RUnlock() | |
| 102 | + | onRatioChanged(pending) | |
| 103 | + | return false | |
| 104 | + | }) | |
| 105 | + | glib.IdleAdd(&cb, 0) | |
| 106 | + | }) | |
| 107 | + | ||
| 108 | + | sv.mu.Unlock() | |
| 109 | + | }) | |
| 110 | + | ||
| 64 | 111 | // Try to apply ratio immediately (in case widget is already allocated) | |
| 65 | 112 | if sv.ApplyRatio() { | |
| 66 | 113 | return sv | |
| @@ -114,6 +161,13 @@ func (sv *SplitView) GetRatio() float64 { | |||
| 114 | 161 | return sv.ratio | |
| 115 | 162 | } | |
| 116 | 163 | ||
| 164 | + | func (sv *SplitView) SetOnRatioChanged(fn func(ratio float64)) { | |
| 165 | + | sv.mu.Lock() | |
| 166 | + | defer sv.mu.Unlock() | |
| 167 | + | ||
| 168 | + | sv.onRatioChanged = fn | |
| 169 | + | } | |
| 170 | + | ||
| 117 | 171 | // ApplyRatio converts the ratio to a pixel position and applies it. | |
| 118 | 172 | // This should be called after the widget has been mapped and has an allocated size. | |
| 119 | 173 | // Returns true if the ratio was successfully applied, false if the widget is not yet allocated. | |
| @@ -3,8 +3,10 @@ package layout | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "sync" | 5 | "sync" | ||
| 6 | + | "time" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/logging" | 8 | "github.com/bnema/dumber/internal/logging" | ||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |||
| 8 | "github.com/rs/zerolog" | 10 | "github.com/rs/zerolog" | ||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| @@ -18,6 +20,10 @@ type SplitView struct { | |||||
| 18 | ratio float64 // 0.0-1.0, relative position of divider | 20 | ratio float64 // 0.0-1.0, relative position of divider | ||
| 19 | logger zerolog.Logger | 21 | logger zerolog.Logger | ||
| 20 | 22 | ||||
| 23 | + | onRatioChanged func(ratio float64) | |||
| 24 | + | pendingNotifyRatio float64 | |||
| 25 | + | notifyDebounceTimer *time.Timer | |||
| 26 | + | ||||
| 21 | mu sync.RWMutex | 27 | mu sync.RWMutex | ||
| 22 | } | 28 | } | ||
| 23 | 29 | ||||
| @@ -25,6 +31,8 @@ type SplitView struct { | |||||
| 25 | // At 60fps, 120 frames = ~2 seconds timeout. | 31 | // At 60fps, 120 frames = ~2 seconds timeout. | ||
| 26 | const maxRetryFrames = 120 | 32 | const maxRetryFrames = 120 | ||
| 27 | 33 | ||||
| 34 | + | const notifyPositionDebounceDelay = 100 * time.Millisecond | |||
| 35 | + | ||||
| 28 | // NewSplitView creates a new split view with the given orientation and children. | 36 | // NewSplitView creates a new split view with the given orientation and children. | ||
| 29 | // The ratio determines the initial divider position (0.0-1.0). | 37 | // The ratio determines the initial divider position (0.0-1.0). | ||
| 30 | func NewSplitView( | 38 | func NewSplitView( | ||
| @@ -61,6 +69,45 @@ func NewSplitView( | |||||
| 61 | paned.SetEndChild(endChild) | 69 | paned.SetEndChild(endChild) | ||
| 62 | } | 70 | } | ||
| 63 | 71 | ||||
| 72 | + | paned.ConnectNotifyPosition(func() { | |||
| 73 | + | position := paned.GetPosition() | |||
| 74 | + | var totalSize int | |||
| 75 | + | if orientation == OrientationHorizontal { | |||
| 76 | + | totalSize = paned.GetAllocatedWidth() | |||
| 77 | + | } else { | |||
| 78 | + | totalSize = paned.GetAllocatedHeight() | |||
| 79 | + | } | |||
| 80 | + | if totalSize <= 0 { | |||
| 81 | + | return | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | ratio := clampRatio(float64(position) / float64(totalSize)) | |||
| 85 | + | ||||
| 86 | + | sv.mu.Lock() | |||
| 87 | + | sv.ratio = ratio | |||
| 88 | + | sv.pendingNotifyRatio = ratio | |||
| 89 | + | onRatioChanged := sv.onRatioChanged | |||
| 90 | + | ||||
| 91 | + | if sv.notifyDebounceTimer != nil { | |||
| 92 | + | sv.notifyDebounceTimer.Stop() | |||
| 93 | + | } | |||
| 94 | + | sv.notifyDebounceTimer = time.AfterFunc(notifyPositionDebounceDelay, func() { | |||
| 95 | + | if onRatioChanged == nil { | |||
| 96 | + | return | |||
| 97 | + | } | |||
| 98 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 99 | + | sv.mu.RLock() | |||
| 100 | + | pending := sv.pendingNotifyRatio | |||
| 101 | + | sv.mu.RUnlock() | |||
| 102 | + | onRatioChanged(pending) | |||
| 103 | + | return false | |||
| 104 | + | }) | |||
| 105 | + | glib.IdleAdd(&cb, 0) | |||
| 106 | + | }) | |||
| 107 | + | ||||
| 108 | + | sv.mu.Unlock() | |||
| 109 | + | }) | |||
| 110 | + | ||||
| 64 | // Try to apply ratio immediately (in case widget is already allocated) | 111 | // Try to apply ratio immediately (in case widget is already allocated) | ||
| 65 | if sv.ApplyRatio() { | 112 | if sv.ApplyRatio() { | ||
| 66 | return sv | 113 | return sv | ||
| @@ -114,6 +161,13 @@ func (sv *SplitView) GetRatio() float64 { | |||||
| 114 | return sv.ratio | 161 | return sv.ratio | ||
| 115 | } | 162 | } | ||
| 116 | 163 | ||||
| 164 | + | func (sv *SplitView) SetOnRatioChanged(fn func(ratio float64)) { | |||
| 165 | + | sv.mu.Lock() | |||
| 166 | + | defer sv.mu.Unlock() | |||
| 167 | + | ||||
| 168 | + | sv.onRatioChanged = fn | |||
| 169 | + | } | |||
| 170 | + | ||||
| 117 | // ApplyRatio converts the ratio to a pixel position and applies it. | 171 | // ApplyRatio converts the ratio to a pixel position and applies it. | ||
| 118 | // This should be called after the widget has been mapped and has an allocated size. | 172 | // This should be called after the widget has been mapped and has an allocated size. | ||
| 119 | // Returns true if the ratio was successfully applied, false if the widget is not yet allocated. | 173 | // Returns true if the ratio was successfully applied, false if the widget is not yet allocated. | ||
M internal/ui/layout/split_test.go +2 -0
| @@ -29,6 +29,8 @@ func setupSplitViewMocks(mockPaned *mocks.MockPanedWidget, orientation layout.Or | |||
| 29 | 29 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 33 | + | ||
| 32 | 34 | // Since allocation is 0, ApplyRatio returns false and callbacks are registered | |
| 33 | 35 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 34 | 36 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -29,6 +29,8 @@ func setupSplitViewMocks(mockPaned *mocks.MockPanedWidget, orientation layout.Or | |||||
| 29 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | 29 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 33 | + | ||||
| 32 | // Since allocation is 0, ApplyRatio returns false and callbacks are registered | 34 | // Since allocation is 0, ApplyRatio returns false and callbacks are registered | ||
| 33 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 35 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 34 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 36 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
M internal/ui/layout/tree.go +19 -1
| @@ -36,7 +36,9 @@ type TreeRenderer struct { | |||
| 36 | 36 | // paneToStack maps pane IDs to their containing StackedView. | |
| 37 | 37 | // Every leaf pane is wrapped in a StackedView for easy stacking later. | |
| 38 | 38 | paneToStack map[string]*StackedView | |
| 39 | - | mu sync.RWMutex | |
| 39 | + | ||
| 40 | + | onSplitRatioChanged func(nodeID string, ratio float64) | |
| 41 | + | mu sync.RWMutex | |
| 40 | 42 | } | |
| 41 | 43 | ||
| 42 | 44 | // NewTreeRenderer creates a new tree renderer. | |
| @@ -54,6 +56,13 @@ func NewTreeRenderer(ctx context.Context, factory WidgetFactory, paneViewFactory | |||
| 54 | 56 | } | |
| 55 | 57 | } | |
| 56 | 58 | ||
| 59 | + | func (tr *TreeRenderer) SetOnSplitRatioChanged(fn func(nodeID string, ratio float64)) { | |
| 60 | + | tr.mu.Lock() | |
| 61 | + | defer tr.mu.Unlock() | |
| 62 | + | ||
| 63 | + | tr.onSplitRatioChanged = fn | |
| 64 | + | } | |
| 65 | + | ||
| 57 | 66 | // Build constructs the entire widget tree from a root PaneNode. | |
| 58 | 67 | // Returns the root widget that can be embedded in a container. | |
| 59 | 68 | func (tr *TreeRenderer) Build(ctx context.Context, root *entity.PaneNode) (Widget, error) { | |
| @@ -170,6 +179,15 @@ func (tr *TreeRenderer) renderSplit(ctx context.Context, node *entity.PaneNode) | |||
| 170 | 179 | ||
| 171 | 180 | // Create split view | |
| 172 | 181 | splitView := NewSplitView(ctx, tr.factory, orientation, leftWidget, rightWidget, node.SplitRatio) | |
| 182 | + | nodeID := node.ID | |
| 183 | + | splitView.SetOnRatioChanged(func(ratio float64) { | |
| 184 | + | tr.mu.RLock() | |
| 185 | + | cb := tr.onSplitRatioChanged | |
| 186 | + | tr.mu.RUnlock() | |
| 187 | + | if cb != nil { | |
| 188 | + | cb(nodeID, ratio) | |
| 189 | + | } | |
| 190 | + | }) | |
| 173 | 191 | tr.splitOrientation[node.ID] = orientation | |
| 174 | 192 | ||
| 175 | 193 | return splitView.Widget(), nil | |
| @@ -36,7 +36,9 @@ type TreeRenderer struct { | |||||
| 36 | // paneToStack maps pane IDs to their containing StackedView. | 36 | // paneToStack maps pane IDs to their containing StackedView. | ||
| 37 | // Every leaf pane is wrapped in a StackedView for easy stacking later. | 37 | // Every leaf pane is wrapped in a StackedView for easy stacking later. | ||
| 38 | paneToStack map[string]*StackedView | 38 | paneToStack map[string]*StackedView | ||
| 39 | - | mu sync.RWMutex | 39 | + | |
| 40 | + | onSplitRatioChanged func(nodeID string, ratio float64) | |||
| 41 | + | mu sync.RWMutex | |||
| 40 | } | 42 | } | ||
| 41 | 43 | ||||
| 42 | // NewTreeRenderer creates a new tree renderer. | 44 | // NewTreeRenderer creates a new tree renderer. | ||
| @@ -54,6 +56,13 @@ func NewTreeRenderer(ctx context.Context, factory WidgetFactory, paneViewFactory | |||||
| 54 | } | 56 | } | ||
| 55 | } | 57 | } | ||
| 56 | 58 | ||||
| 59 | + | func (tr *TreeRenderer) SetOnSplitRatioChanged(fn func(nodeID string, ratio float64)) { | |||
| 60 | + | tr.mu.Lock() | |||
| 61 | + | defer tr.mu.Unlock() | |||
| 62 | + | ||||
| 63 | + | tr.onSplitRatioChanged = fn | |||
| 64 | + | } | |||
| 65 | + | ||||
| 57 | // Build constructs the entire widget tree from a root PaneNode. | 66 | // Build constructs the entire widget tree from a root PaneNode. | ||
| 58 | // Returns the root widget that can be embedded in a container. | 67 | // Returns the root widget that can be embedded in a container. | ||
| 59 | func (tr *TreeRenderer) Build(ctx context.Context, root *entity.PaneNode) (Widget, error) { | 68 | func (tr *TreeRenderer) Build(ctx context.Context, root *entity.PaneNode) (Widget, error) { | ||
| @@ -170,6 +179,15 @@ func (tr *TreeRenderer) renderSplit(ctx context.Context, node *entity.PaneNode) | |||||
| 170 | 179 | ||||
| 171 | // Create split view | 180 | // Create split view | ||
| 172 | splitView := NewSplitView(ctx, tr.factory, orientation, leftWidget, rightWidget, node.SplitRatio) | 181 | splitView := NewSplitView(ctx, tr.factory, orientation, leftWidget, rightWidget, node.SplitRatio) | ||
| 182 | + | nodeID := node.ID | |||
| 183 | + | splitView.SetOnRatioChanged(func(ratio float64) { | |||
| 184 | + | tr.mu.RLock() | |||
| 185 | + | cb := tr.onSplitRatioChanged | |||
| 186 | + | tr.mu.RUnlock() | |||
| 187 | + | if cb != nil { | |||
| 188 | + | cb(nodeID, ratio) | |||
| 189 | + | } | |||
| 190 | + | }) | |||
| 173 | tr.splitOrientation[node.ID] = orientation | 191 | tr.splitOrientation[node.ID] = orientation | ||
| 174 | 192 | ||||
| 175 | return splitView.Widget(), nil | 193 | return splitView.Widget(), nil | ||
M internal/ui/layout/tree_test.go +7 -0
| @@ -123,6 +123,7 @@ func TestBuild_HorizontalSplit(t *testing.T) { | |||
| 123 | 123 | mockRightStackBox.EXPECT().SetVisible(true).Once() | |
| 124 | 124 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | |
| 125 | 125 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | |
| 126 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 126 | 127 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 127 | 128 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 128 | 129 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -172,6 +173,7 @@ func TestBuild_VerticalSplit(t *testing.T) { | |||
| 172 | 173 | mockBottomStackBox.EXPECT().SetVisible(true).Once() | |
| 173 | 174 | mockPaned.EXPECT().SetStartChild(mockTopStackBox).Once() | |
| 174 | 175 | mockPaned.EXPECT().SetEndChild(mockBottomStackBox).Once() | |
| 176 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 175 | 177 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | |
| 176 | 178 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 177 | 179 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -240,6 +242,7 @@ func TestBuild_NestedSplits(t *testing.T) { | |||
| 240 | 242 | mockStackBox2.EXPECT().SetVisible(true).Once() | |
| 241 | 243 | mockInnerPaned.EXPECT().SetStartChild(mockStackBox1).Once() | |
| 242 | 244 | mockInnerPaned.EXPECT().SetEndChild(mockStackBox2).Once() | |
| 245 | + | mockInnerPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 243 | 246 | mockInnerPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | |
| 244 | 247 | mockInnerPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 245 | 248 | mockInnerPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -253,6 +256,7 @@ func TestBuild_NestedSplits(t *testing.T) { | |||
| 253 | 256 | mockStackBox3.EXPECT().SetVisible(true).Once() | |
| 254 | 257 | mockOuterPaned.EXPECT().SetStartChild(mockInnerPaned).Once() | |
| 255 | 258 | mockOuterPaned.EXPECT().SetEndChild(mockStackBox3).Once() | |
| 259 | + | mockOuterPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 256 | 260 | mockOuterPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 257 | 261 | mockOuterPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 258 | 262 | mockOuterPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -414,6 +418,7 @@ func TestGetNodeIDs_ReturnsAllIDs(t *testing.T) { | |||
| 414 | 418 | mockRightStackBox.EXPECT().SetVisible(true).Once() | |
| 415 | 419 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | |
| 416 | 420 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | |
| 421 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 417 | 422 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 418 | 423 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 419 | 424 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| @@ -538,8 +543,10 @@ func TestUpdateSplitRatio_ExistingNode(t *testing.T) { | |||
| 538 | 543 | mockRightStackBox.EXPECT().SetVisible(true).Once() | |
| 539 | 544 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | |
| 540 | 545 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | |
| 546 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |
| 541 | 547 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Twice() | |
| 542 | 548 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 549 | + | ||
| 543 | 550 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| 544 | 551 | ||
| 545 | 552 | renderer := layout.NewTreeRenderer(ctx, mockFactory, mockPaneViewFactory) | |
| @@ -123,6 +123,7 @@ func TestBuild_HorizontalSplit(t *testing.T) { | |||||
| 123 | mockRightStackBox.EXPECT().SetVisible(true).Once() | 123 | mockRightStackBox.EXPECT().SetVisible(true).Once() | ||
| 124 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | 124 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | ||
| 125 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | 125 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | ||
| 126 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 126 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | 127 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | ||
| 127 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 128 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 128 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 129 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| @@ -172,6 +173,7 @@ func TestBuild_VerticalSplit(t *testing.T) { | |||||
| 172 | mockBottomStackBox.EXPECT().SetVisible(true).Once() | 173 | mockBottomStackBox.EXPECT().SetVisible(true).Once() | ||
| 173 | mockPaned.EXPECT().SetStartChild(mockTopStackBox).Once() | 174 | mockPaned.EXPECT().SetStartChild(mockTopStackBox).Once() | ||
| 174 | mockPaned.EXPECT().SetEndChild(mockBottomStackBox).Once() | 175 | mockPaned.EXPECT().SetEndChild(mockBottomStackBox).Once() | ||
| 176 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 175 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | 177 | mockPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | ||
| 176 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 178 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 177 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 179 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| @@ -240,6 +242,7 @@ func TestBuild_NestedSplits(t *testing.T) { | |||||
| 240 | mockStackBox2.EXPECT().SetVisible(true).Once() | 242 | mockStackBox2.EXPECT().SetVisible(true).Once() | ||
| 241 | mockInnerPaned.EXPECT().SetStartChild(mockStackBox1).Once() | 243 | mockInnerPaned.EXPECT().SetStartChild(mockStackBox1).Once() | ||
| 242 | mockInnerPaned.EXPECT().SetEndChild(mockStackBox2).Once() | 244 | mockInnerPaned.EXPECT().SetEndChild(mockStackBox2).Once() | ||
| 245 | + | mockInnerPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 243 | mockInnerPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | 246 | mockInnerPaned.EXPECT().GetAllocatedHeight().Return(0).Once() | ||
| 244 | mockInnerPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 247 | mockInnerPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 245 | mockInnerPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 248 | mockInnerPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| @@ -253,6 +256,7 @@ func TestBuild_NestedSplits(t *testing.T) { | |||||
| 253 | mockStackBox3.EXPECT().SetVisible(true).Once() | 256 | mockStackBox3.EXPECT().SetVisible(true).Once() | ||
| 254 | mockOuterPaned.EXPECT().SetStartChild(mockInnerPaned).Once() | 257 | mockOuterPaned.EXPECT().SetStartChild(mockInnerPaned).Once() | ||
| 255 | mockOuterPaned.EXPECT().SetEndChild(mockStackBox3).Once() | 258 | mockOuterPaned.EXPECT().SetEndChild(mockStackBox3).Once() | ||
| 259 | + | mockOuterPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 256 | mockOuterPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | 260 | mockOuterPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | ||
| 257 | mockOuterPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 261 | mockOuterPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 258 | mockOuterPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 262 | mockOuterPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| @@ -414,6 +418,7 @@ func TestGetNodeIDs_ReturnsAllIDs(t *testing.T) { | |||||
| 414 | mockRightStackBox.EXPECT().SetVisible(true).Once() | 418 | mockRightStackBox.EXPECT().SetVisible(true).Once() | ||
| 415 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | 419 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | ||
| 416 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | 420 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | ||
| 421 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 417 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | 422 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | ||
| 418 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 423 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 419 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 424 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| @@ -538,8 +543,10 @@ func TestUpdateSplitRatio_ExistingNode(t *testing.T) { | |||||
| 538 | mockRightStackBox.EXPECT().SetVisible(true).Once() | 543 | mockRightStackBox.EXPECT().SetVisible(true).Once() | ||
| 539 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | 544 | mockPaned.EXPECT().SetStartChild(mockLeftStackBox).Once() | ||
| 540 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | 545 | mockPaned.EXPECT().SetEndChild(mockRightStackBox).Once() | ||
| 546 | + | mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint32(0)).Once() | |||
| 541 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Twice() | 547 | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Twice() | ||
| 542 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 548 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 549 | + | ||||
| 543 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | 550 | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | ||
| 544 | 551 | ||||
| 545 | renderer := layout.NewTreeRenderer(ctx, mockFactory, mockPaneViewFactory) | 552 | renderer := layout.NewTreeRenderer(ctx, mockFactory, mockPaneViewFactory) | ||
M internal/ui/layout/widgets.go +1 -0
| @@ -95,6 +95,7 @@ type PanedWidget interface { | |||
| 95 | 95 | ||
| 96 | 96 | // Signals | |
| 97 | 97 | ConnectMap(callback func()) uint32 | |
| 98 | + | ConnectNotifyPosition(callback func()) uint32 | |
| 98 | 99 | ||
| 99 | 100 | // Tick callback for frame-based updates (returns signal ID) | |
| 100 | 101 | // Callback returns true to continue, false to stop | |
| @@ -95,6 +95,7 @@ type PanedWidget interface { | |||||
| 95 | 95 | ||||
| 96 | // Signals | 96 | // Signals | ||
| 97 | ConnectMap(callback func()) uint32 | 97 | ConnectMap(callback func()) uint32 | ||
| 98 | + | ConnectNotifyPosition(callback func()) uint32 | |||
| 98 | 99 | ||||
| 99 | // Tick callback for frame-based updates (returns signal ID) | 100 | // Tick callback for frame-based updates (returns signal ID) | ||
| 100 | // Callback returns true to continue, false to stop | 101 | // Callback returns true to continue, false to stop | ||