@@ -2,19 +2,37 @@ package ui
22
33import (
44 "fmt"
5+ "time"
56 "zfs-file-history/internal/logging"
67 "zfs-file-history/internal/ui/dataset_info"
78 "zfs-file-history/internal/ui/file_browser"
89 "zfs-file-history/internal/ui/shortcut_helper"
910 "zfs-file-history/internal/ui/snapshot_browser"
1011 "zfs-file-history/internal/ui/status_message"
12+ "zfs-file-history/internal/ui/theme"
1113 uiutil "zfs-file-history/internal/ui/util"
1214 "zfs-file-history/internal/zfs"
1315
1416 "github.com/gdamore/tcell/v2"
1517 "github.com/rivo/tview"
1618)
1719
20+ type dragType int
21+
22+ const (
23+ dragNone dragType = iota
24+ dragVertical
25+ dragHorizontal
26+ )
27+
28+ type boundaryType int
29+
30+ const (
31+ boundaryNone boundaryType = iota
32+ boundaryVertical
33+ boundaryHorizontal
34+ )
35+
1836type MainPage struct {
1937 application * tview.Application
2038 header * ApplicationHeaderComponent
@@ -23,8 +41,16 @@ type MainPage struct {
2341 datasetInfo * dataset_info.DatasetInfoComponent
2442 snapshotBrowser * snapshot_browser.SnapshotBrowserComponent
2543 layout * tview.Flex
44+ windowLayout * tview.Flex
45+ infoLayout * tview.Flex
2646
2747 wasInitialized bool
48+
49+ isDragging bool
50+ dragType dragType
51+ hoveredBoundary boundaryType
52+ lastDragRedraw time.Time
53+ dragTimer * time.Timer
2854}
2955
3056func NewMainPage (application * tview.Application , path string ) * MainPage {
@@ -143,6 +169,140 @@ func (mainPage *MainPage) createLayout() *tview.Flex {
143169
144170 mainPageLayout .AddItem (windowLayout , 0 , 1 , true )
145171
172+ mainPage .windowLayout = windowLayout
173+ mainPage .infoLayout = infoLayout
174+
175+ // Set mouse capture on the top-level layout to capture drags anywhere on the screen
176+ mainPageLayout .SetMouseCapture (func (action tview.MouseAction , event * tcell.EventMouse ) (tview.MouseAction , * tcell.EventMouse ) {
177+ mouseX , mouseY := event .Position ()
178+ buttons := event .Buttons ()
179+
180+ diX , diY , diW , diH := mainPage .datasetInfo .GetLayout ().GetRect ()
181+ _ , sbY , _ , sbH := mainPage .snapshotBrowser .GetLayout ().GetRect ()
182+ winX , _ , winW , _ := windowLayout .GetRect ()
183+
184+ // 1. If currently dragging
185+ if mainPage .isDragging {
186+ if buttons == tcell .ButtonNone || action == tview .MouseLeftUp {
187+ mainPage .isDragging = false
188+ mainPage .dragType = dragNone
189+ mainPage .hoveredBoundary = boundaryNone
190+ if mainPage .dragTimer != nil {
191+ mainPage .dragTimer .Stop ()
192+ mainPage .dragTimer = nil
193+ }
194+ mainPage .updateBorderHighlights ()
195+ return tview .MouseConsumed , nil
196+ }
197+
198+ // Rate limit updates to 30ms to prevent redraw flooding/input lag
199+ now := time .Now ()
200+ if now .Sub (mainPage .lastDragRedraw ) > 30 * time .Millisecond {
201+ mainPage .lastDragRedraw = now
202+ if mainPage .dragTimer != nil {
203+ mainPage .dragTimer .Stop ()
204+ mainPage .dragTimer = nil
205+ }
206+ mainPage .applyResize (mouseX , mouseY , winX , winW , diY , diH , sbY , sbH )
207+ return tview .MouseConsumed , nil
208+ } else {
209+ // Schedule a trailing redraw for the final drag position
210+ if mainPage .dragTimer != nil {
211+ mainPage .dragTimer .Stop ()
212+ }
213+ mainPage .dragTimer = time .AfterFunc (30 * time .Millisecond , func () {
214+ mainPage .application .QueueUpdateDraw (func () {
215+ mainPage .applyResize (mouseX , mouseY , winX , winW , diY , diH , sbY , sbH )
216+ })
217+ })
218+ return action , nil // consume event for children but do not trigger immediate screen redraw
219+ }
220+ }
221+
222+ // 2. Not dragging: detect hover boundaries
223+ isOnVertical := false
224+ if mouseY >= diY && mouseY < sbY + sbH {
225+ if mouseX == diX || mouseX == diX - 1 {
226+ isOnVertical = true
227+ }
228+ }
229+
230+ isOnHorizontal := false
231+ if mouseX >= diX && mouseX < diX + diW {
232+ if mouseY == sbY || mouseY == sbY - 1 {
233+ isOnHorizontal = true
234+ }
235+ }
236+
237+ newHover := boundaryNone
238+ if isOnHorizontal {
239+ newHover = boundaryHorizontal
240+ } else if isOnVertical {
241+ newHover = boundaryVertical
242+ }
243+
244+ if newHover != mainPage .hoveredBoundary {
245+ mainPage .hoveredBoundary = newHover
246+ mainPage .updateBorderHighlights ()
247+ return tview .MouseConsumed , nil
248+ }
249+
250+ // 3. Initiate dragging
251+ if buttons & tcell .Button1 != 0 && action == tview .MouseLeftDown {
252+ if isOnHorizontal {
253+ mainPage .isDragging = true
254+ mainPage .dragType = dragHorizontal
255+ mainPage .lastDragRedraw = time .Now ()
256+ return tview .MouseConsumed , nil
257+ } else if isOnVertical {
258+ mainPage .isDragging = true
259+ mainPage .dragType = dragVertical
260+ mainPage .lastDragRedraw = time .Now ()
261+ return tview .MouseConsumed , nil
262+ }
263+ }
264+
265+ return action , event
266+ })
267+
268+ // Configure drawing of highlighted adjacent borders after the screen draws
269+ mainPage .application .SetAfterDrawFunc (func (screen tcell.Screen ) {
270+ // Highlight vertical boundary adjacent line segment
271+ if mainPage .hoveredBoundary == boundaryVertical || (mainPage .isDragging && mainPage .dragType == dragVertical ) {
272+ _ , diY , _ , _ := mainPage .datasetInfo .GetLayout ().GetRect ()
273+ diX , _ , diW , _ := mainPage .datasetInfo .GetLayout ().GetRect ()
274+ _ , sbY , _ , sbH := mainPage .snapshotBrowser .GetLayout ().GetRect ()
275+
276+ if diW > 0 && sbH > 0 {
277+ highlightColor := theme .Primary
278+ for y := diY ; y < sbY + sbH ; y ++ {
279+ for _ , x := range []int {diX - 1 , diX } {
280+ primary , combining , style , _ := screen .GetContent (x , y )
281+ newStyle := style .Foreground (highlightColor )
282+ screen .SetContent (x , y , primary , combining , newStyle )
283+ }
284+ }
285+ }
286+ }
287+
288+ // Highlight horizontal boundary adjacent line segment
289+ if mainPage .hoveredBoundary == boundaryHorizontal || (mainPage .isDragging && mainPage .dragType == dragHorizontal ) {
290+ diX , _ , diW , _ := mainPage .datasetInfo .GetLayout ().GetRect ()
291+ _ , sbY , _ , sbH := mainPage .snapshotBrowser .GetLayout ().GetRect ()
292+
293+ if diW > 0 && sbH > 0 {
294+ highlightColor := theme .Primary
295+ for x := diX ; x < diX + diW ; x ++ {
296+ for _ , y := range []int {sbY - 1 , sbY } {
297+ primary , combining , style , _ := screen .GetContent (x , y )
298+ newStyle := style .Foreground (highlightColor )
299+ screen .SetContent (x , y , primary , combining , newStyle )
300+ }
301+ }
302+ }
303+ }
304+ })
305+
146306 mainPage .header = header
147307
148308 shortcutMap := shortcut_helper .NewShortcutMap (mainPage .application )
@@ -218,3 +378,40 @@ func (mainPage *MainPage) updateShortcutMap(component FocusableUiComponent) {
218378 mainPage .clearShortcutMap ()
219379 }
220380}
381+
382+ func (mainPage * MainPage ) updateBorderHighlights () {
383+ // Redraw logic is handled by SetAfterDrawFunc based on the hoveredBoundary/isDragging states.
384+ }
385+
386+ func (mainPage * MainPage ) applyResize (mouseX , mouseY , winX , winW , diY , diH , sbY , sbH int ) {
387+ if mainPage .dragType == dragVertical {
388+ newLeftWidth := mouseX - winX
389+ minWidth := 10
390+ if newLeftWidth < minWidth {
391+ newLeftWidth = minWidth
392+ }
393+ if newLeftWidth > winW - minWidth {
394+ newLeftWidth = winW - minWidth
395+ }
396+ newRightWidth := winW - newLeftWidth
397+
398+ mainPage .windowLayout .ResizeItem (mainPage .fileBrowser .GetLayout (), 0 , newLeftWidth )
399+ mainPage .windowLayout .ResizeItem (mainPage .infoLayout , 0 , newRightWidth )
400+ } else if mainPage .dragType == dragHorizontal {
401+ infoH := diH + sbH
402+ infoY := diY
403+ newTopHeight := mouseY - infoY
404+ minTopHeight := 4
405+ minBottomHeight := 5
406+ if newTopHeight < minTopHeight {
407+ newTopHeight = minTopHeight
408+ }
409+ if newTopHeight > infoH - minBottomHeight {
410+ newTopHeight = infoH - minBottomHeight
411+ }
412+ newBottomHeight := infoH - newTopHeight
413+
414+ mainPage .infoLayout .ResizeItem (mainPage .datasetInfo .GetLayout (), 0 , newTopHeight )
415+ mainPage .infoLayout .ResizeItem (mainPage .snapshotBrowser .GetLayout (), 0 , newBottomHeight )
416+ }
417+ }
0 commit comments