Skip to content

Commit f9ee6df

Browse files
authored
Merge pull request #130 from markusressel/feature/window-resizing
Real-Time Mouse-Based Window Resizing
2 parents 07f23d3 + 06bc04c commit f9ee6df

5 files changed

Lines changed: 233 additions & 0 deletions

File tree

internal/ui/dataset_info/dataset_info.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ func (datasetInfo *DatasetInfoComponent) GetLayout() *uiutil.LoadingContainer {
212212
return datasetInfo.container
213213
}
214214

215+
func (datasetInfo *DatasetInfoComponent) SetBorderColor(color tcell.Color) {
216+
datasetInfo.textView.SetBorderColor(color)
217+
if datasetInfo.container != nil {
218+
datasetInfo.container.SetBorderColor(color)
219+
}
220+
}
221+
215222
func (datasetInfo *DatasetInfoComponent) CreateSnapshot(name string) error {
216223
if datasetInfo.dataset == nil {
217224
return fmt.Errorf("no dataset selected")

internal/ui/file_browser/file_browser.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,12 @@ func (fileBrowser *FileBrowserComponent) GetLayout() tview.Primitive {
888888
return fileBrowser.layout
889889
}
890890

891+
func (fileBrowser *FileBrowserComponent) SetBorderColor(color tcell.Color) {
892+
if flex, ok := fileBrowser.tableContainer.GetLayout().(*tview.Flex); ok {
893+
flex.SetBorderColor(color)
894+
}
895+
}
896+
891897
func (fileBrowser *FileBrowserComponent) SelectHeader() {
892898
fileBrowser.tableContainer.SelectHeader()
893899
}

internal/ui/main_page.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,37 @@ package ui
22

33
import (
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+
1836
type 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

3056
func 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+
}

internal/ui/snapshot_browser/snapshot_browser.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ func (snapshotBrowser *SnapshotBrowserComponent) GetLayout() *uiutil.LoadingCont
202202
return snapshotBrowser.container
203203
}
204204

205+
func (snapshotBrowser *SnapshotBrowserComponent) SetBorderColor(color tcell.Color) {
206+
if flex, ok := snapshotBrowser.tableContainer.GetLayout().(*tview.Flex); ok {
207+
flex.SetBorderColor(color)
208+
}
209+
if snapshotBrowser.container != nil {
210+
snapshotBrowser.container.SetBorderColor(color)
211+
}
212+
}
213+
205214
func (snapshotBrowser *SnapshotBrowserComponent) SetPath(path string, force bool) {
206215
if !force && snapshotBrowser.path == path {
207216
return

internal/ui/util/data_loader.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"sync"
66
"sync/atomic"
77

8+
"github.com/gdamore/tcell/v2"
89
"github.com/rivo/tview"
910
)
1011

@@ -112,3 +113,16 @@ func (c *LoadingContainer) SetIsLoading(isLoading bool) {
112113
func (c *LoadingContainer) GetFrontPage() (string, tview.Primitive) {
113114
return c.Pages.GetFrontPage()
114115
}
116+
117+
func (c *LoadingContainer) SetBorderColor(color tcell.Color) {
118+
if c.loadingView != nil {
119+
c.loadingView.SetBorderColor(color)
120+
}
121+
if c.content != nil {
122+
if box, ok := c.content.(interface{ SetBorderColor(tcell.Color) *tview.Box }); ok {
123+
box.SetBorderColor(color)
124+
} else if flex, ok := c.content.(*tview.Flex); ok {
125+
flex.SetBorderColor(color)
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)