Release Please / Release Please (push) Skipped
Tests / Test (Go 1.22) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Release / GoReleaser (push) Failing after 38s
Polish the office TUI with proportional pane layout, drag resize, live filtering, HTML mail preview, paginated mail lists, overflow scrollbars, flattened navigation, and project save/status fixes; add UpdateProjectStatus. Co-authored-by: Cursor <cursoragent@cursor.com>
146 lines
3.0 KiB
Go
146 lines
3.0 KiB
Go
package ui
|
|
|
|
import "github.com/eslider/go-onlyoffice/cmd/office/model"
|
|
|
|
// PaneVisibility controls which columns are shown.
|
|
type PaneVisibility struct {
|
|
Menu, List, Detail bool
|
|
}
|
|
|
|
// PaneWidths is outer lipgloss width per column (sums to terminal width).
|
|
type PaneWidths struct {
|
|
Menu, List, Detail int
|
|
Visibility PaneVisibility
|
|
}
|
|
|
|
func defaultPaneVisibility() PaneVisibility {
|
|
return PaneVisibility{Menu: true, List: true, Detail: true}
|
|
}
|
|
|
|
const (
|
|
defaultMenuShare = 10
|
|
defaultListShare = 60
|
|
defaultDetailShare = 30
|
|
)
|
|
|
|
// LayoutWidths splits total terminal width across visible panes (default 10% / 60% / 30%).
|
|
func LayoutWidths(total int, vis PaneVisibility) PaneWidths {
|
|
if total < 1 {
|
|
total = 80
|
|
}
|
|
v := vis
|
|
if countVisible(v) == 0 {
|
|
v = defaultPaneVisibility()
|
|
}
|
|
shareSum := 0
|
|
if v.Menu {
|
|
shareSum += defaultMenuShare
|
|
}
|
|
if v.List {
|
|
shareSum += defaultListShare
|
|
}
|
|
if v.Detail {
|
|
shareSum += defaultDetailShare
|
|
}
|
|
assign := func(visible bool, share int) int {
|
|
if !visible || shareSum == 0 {
|
|
return 0
|
|
}
|
|
return total * share / shareSum
|
|
}
|
|
out := PaneWidths{Visibility: v}
|
|
out.Menu = assign(v.Menu, defaultMenuShare)
|
|
out.List = assign(v.List, defaultListShare)
|
|
out.Detail = assign(v.Detail, defaultDetailShare)
|
|
fixPaneWidthSum(&out, total)
|
|
return out
|
|
}
|
|
|
|
// DetailPaneXRange returns the [start, end) column span of the detail pane.
|
|
func DetailPaneXRange(pw PaneWidths) (start, end int) {
|
|
if !pw.Visibility.Detail {
|
|
return 0, 0
|
|
}
|
|
if pw.Visibility.Menu {
|
|
start += pw.Menu
|
|
}
|
|
if pw.Visibility.List {
|
|
start += pw.List
|
|
}
|
|
end = start + pw.Detail
|
|
return start, end
|
|
}
|
|
|
|
func countVisible(v PaneVisibility) int {
|
|
n := 0
|
|
if v.Menu {
|
|
n++
|
|
}
|
|
if v.List {
|
|
n++
|
|
}
|
|
if v.Detail {
|
|
n++
|
|
}
|
|
return n
|
|
}
|
|
|
|
// NextVisibleFocus cycles focus forward, skipping hidden panes.
|
|
func NextVisibleFocus(cur model.FocusPane, vis PaneVisibility) model.FocusPane {
|
|
order := []model.FocusPane{model.FocusMenu, model.FocusList, model.FocusPreview}
|
|
start := 0
|
|
for i, p := range order {
|
|
if p == cur {
|
|
start = i
|
|
break
|
|
}
|
|
}
|
|
for step := 1; step <= len(order); step++ {
|
|
p := order[(start+step)%len(order)]
|
|
if paneVisible(p, vis) {
|
|
return p
|
|
}
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// PrevVisibleFocus cycles focus backward, skipping hidden panes.
|
|
func PrevVisibleFocus(cur model.FocusPane, vis PaneVisibility) model.FocusPane {
|
|
order := []model.FocusPane{model.FocusMenu, model.FocusList, model.FocusPreview}
|
|
start := 0
|
|
for i, p := range order {
|
|
if p == cur {
|
|
start = i
|
|
break
|
|
}
|
|
}
|
|
for step := 1; step <= len(order); step++ {
|
|
p := order[(start-step+len(order))%len(order)]
|
|
if paneVisible(p, vis) {
|
|
return p
|
|
}
|
|
}
|
|
return cur
|
|
}
|
|
|
|
func paneVisible(p model.FocusPane, vis PaneVisibility) bool {
|
|
switch p {
|
|
case model.FocusMenu:
|
|
return vis.Menu
|
|
case model.FocusList:
|
|
return vis.List
|
|
default:
|
|
return vis.Detail
|
|
}
|
|
}
|
|
|
|
func firstVisibleFocus(vis PaneVisibility) model.FocusPane {
|
|
if vis.Menu {
|
|
return model.FocusMenu
|
|
}
|
|
if vis.List {
|
|
return model.FocusList
|
|
}
|
|
return model.FocusPreview
|
|
}
|