diff --git a/go.mod b/go.mod index 30f34bf..dca372d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/andybalholm/brotli v1.2.6 github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e - github.com/ayn2op/tview v0.0.0-20261001183738-08b3e2ae6452 + github.com/ayn2op/tview v0.0.0-20261002024202-f7619deb5073 github.com/bogdanfinn/fhttp v0.6.9 github.com/bogdanfinn/tls-client v1.16.0 github.com/gdamore/tcell/v3 v3.5.0 diff --git a/go.sum b/go.sum index 4fe0132..d7d46d0 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c h1:Qkl3W5RXzUQUR github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c/go.mod h1:SHqcr+EmZKeNtVKZybHJn4HNz+ufA1XbeKNSTigBCEE= github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e h1:QAei/IVEyZV7GcpkpcFUh37+QRqwrt57z7OT3ciHrhI= github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e/go.mod h1:RsvYiMzi4Si1F7vmml5NCL2DaKIZXkNUQC7QlGTMtCw= -github.com/ayn2op/tview v0.0.0-20261001183738-08b3e2ae6452 h1:DXlBoek+AEXAxZVnOSu/ld06rwr+e7IsalIpAdegP8I= -github.com/ayn2op/tview v0.0.0-20261001183738-08b3e2ae6452/go.mod h1:oRPTSZgaEJPhv3dmmNvo6grBq/BemrLG3wFQLMTZJkY= +github.com/ayn2op/tview v0.0.0-20261002024202-f7619deb5073 h1:ipmYpvyH7prpXsLYDYWFAgXLC++xr8ss6x7nlgCOhvU= +github.com/ayn2op/tview v0.0.0-20261002024202-f7619deb5073/go.mod h1:oRPTSZgaEJPhv3dmmNvo6grBq/BemrLG3wFQLMTZJkY= github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= diff --git a/internal/ui/chat/composer/model.go b/internal/ui/chat/composer/model.go index 7d263a7..98c68cb 100644 --- a/internal/ui/chat/composer/model.go +++ b/internal/ui/chat/composer/model.go @@ -4,6 +4,7 @@ import ( "bytes" "cmp" "context" + "github.com/ayn2op/tview/layout" "io" "log/slog" "regexp" @@ -117,6 +118,14 @@ type view struct { focused bool } +func (v view) Size() (width, height layout.Length) { + return v.box(v.focused).Size() +} + +func (v view) Layout(limits layout.Limits) layout.Size { + return v.box(v.focused).Layout(limits) +} + func (v view) Draw(screen tview.Screen, area tview.Rectangle) { v.box(v.focused).Draw(screen, area) } @@ -492,6 +501,14 @@ type mentionsPopup struct { c *Model } +// Size returns Fill, as the popup places the list within its whole area. +func (mentionsPopup) Size() (width, height layout.Length) { return layout.Fill, layout.Fill } + +// Layout returns the size of limits, as the popup places the list within its whole area. +func (mentionsPopup) Layout(limits layout.Limits) layout.Size { + return layout.Atomic(limits, layout.Fill, layout.Fill) +} + func (p mentionsPopup) Draw(screen tview.Screen, area tview.Rectangle) { p.c.mentionsList.View().Draw(screen, p.area(area)) } diff --git a/internal/ui/chat/guildstree/model.go b/internal/ui/chat/guildstree/model.go index 7062adf..7d57f93 100644 --- a/internal/ui/chat/guildstree/model.go +++ b/internal/ui/chat/guildstree/model.go @@ -50,7 +50,7 @@ func (m *Model) CurrentNode() *tree.Node { func (m Model) View(focused bool) tview.Element { onChange := func(c tree.Change) tview.Msg { return Msg(c) } onSelect := func(n *tree.Node) tview.Msg { return SelectedMsg{Node: n} } - return uitree.New(m.root, &m.selectionState, m.cfg, m.cfg.Theme.GuildsTree.CommonTreeTheme, m.cfg.Keybinds.GuildsTree, focused, onChange, onSelect).Title("Guilds") + return uitree.New(m.root, m.selectionState, m.cfg, m.cfg.Theme.GuildsTree.CommonTreeTheme, m.cfg.Keybinds.GuildsTree, focused, onChange, onSelect).Title("Guilds") } func (m *Model) reset() *tree.Node { diff --git a/internal/ui/chat/memberstree/model.go b/internal/ui/chat/memberstree/model.go index 9d23ecd..88904eb 100644 --- a/internal/ui/chat/memberstree/model.go +++ b/internal/ui/chat/memberstree/model.go @@ -85,7 +85,7 @@ func (m *Model) SetChannel(channel *discord.Channel) tview.Cmd { func (m Model) View(focused bool) tview.Element { onChange := func(c tree.Change) tview.Msg { return Msg(c) } onSelect := func(n *tree.Node) tview.Msg { return SelectedMsg{Node: n} } - return uitree.New(m.root, &m.selectionState, m.cfg, m.cfg.Theme.MembersTree, m.cfg.Keybinds.MembersTree, focused, onChange, onSelect).Title("Members") + return uitree.New(m.root, m.selectionState, m.cfg, m.cfg.Theme.MembersTree, m.cfg.Keybinds.MembersTree, focused, onChange, onSelect).Title("Members") } func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { diff --git a/internal/ui/chat/mentionslist/model.go b/internal/ui/chat/mentionslist/model.go index 523da19..b777359 100644 --- a/internal/ui/chat/mentionslist/model.go +++ b/internal/ui/chat/mentionslist/model.go @@ -4,6 +4,7 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview" + "github.com/ayn2op/tview/layout" "github.com/ayn2op/tview/list" "github.com/gdamore/tcell/v3" "github.com/rivo/uniseg" @@ -30,7 +31,7 @@ func (m Model) View() tview.Element { func (Model) Init() tview.Cmd { return nil } func (m Model) listView() list.Widget { - return list.New(&m.selectionState, len(m.items), func(i int) list.Item { + return list.New(m.selectionState, len(m.items), func(i int) tview.Element { item := m.items[i] return row{text: item.DisplayText, style: item.Style} }). @@ -89,7 +90,13 @@ type row struct { style tcell.Style } -func (row) Rows(int) int { return 1 } +// Size returns Fill and a height of one row. +func (row) Size() (width, height layout.Length) { return layout.Fill, layout.Fixed(1) } + +// Layout returns a height of one row. +func (row) Layout(limits layout.Limits) layout.Size { + return layout.Atomic(limits, layout.Fill, layout.Fixed(1)) +} func (r row) Draw(screen tview.Screen, area tview.Rectangle) { for x := area.X; x < area.X+area.Width; x++ { diff --git a/internal/ui/chat/messageslist/model.go b/internal/ui/chat/messageslist/model.go index a970224..3cf2162 100644 --- a/internal/ui/chat/messageslist/model.go +++ b/internal/ui/chat/messageslist/model.go @@ -2,6 +2,7 @@ package messageslist import ( "fmt" + "github.com/ayn2op/tview/layout" "io" "log/slog" "net/http" @@ -36,7 +37,7 @@ import ( type messageItem struct { message discord.Message - view list.Item + view tview.Element separator bool timestamp discord.Timestamp @@ -106,7 +107,7 @@ func (ml *Model) title() string { } func (ml *Model) listView(focused bool) list.Widget { - return list.New(&ml.selectionState, len(ml.items), ml.buildItem). + return list.New(ml.selectionState, len(ml.items), ml.buildItem). SelectedStyle(ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style). ScrollBar(ml.scrollBar, ml.scrollBarVisibility). Keybind(scrollKeybind(ml.cfg.Keybinds.MessagesList.ScrollKeybinds)). @@ -239,7 +240,7 @@ func (ml *Model) clearSelection() { ml.setCursor(-1) } -func (ml *Model) buildItem(index int) list.Item { +func (ml *Model) buildItem(index int) tview.Element { if index < 0 || index >= len(ml.items) { return nil } @@ -284,14 +285,20 @@ func (v *messageView) at(width int) textview.Widget { return textview.New(v.lines).Wrap(false) } -func (v *messageView) Rows(width int) int { - v.at(width) - rows := len(v.lines) - for _, preview := range v.images { - _, height := preview.Size() - rows += height.Cells() - } - return rows +// Size returns Fill and Shrink, as the message is as tall as its content. +func (v *messageView) Size() (width, height layout.Length) { return layout.Fill, layout.Shrink } + +// Layout returns the size of the message within limits, as tall as the rows it takes at the width of limits. +func (v *messageView) Layout(limits layout.Limits) layout.Size { + return layout.Sized(limits, layout.Fill, layout.Shrink, func(limits layout.Limits) layout.Size { + v.at(limits.Max.Width) + rows := len(v.lines) + for _, preview := range v.images { + _, height := preview.Size() + rows += height.Cells() + } + return layout.Size{Height: rows} + }) } func (v *messageView) Draw(screen tview.Screen, area tview.Rectangle) { @@ -312,7 +319,13 @@ type dateSeparator struct { style tcell.Style } -func (dateSeparator) Rows(int) int { return 1 } +// Size returns Fill and a height of one row. +func (dateSeparator) Size() (width, height layout.Length) { return layout.Fill, layout.Fixed(1) } + +// Layout returns a height of one row. +func (dateSeparator) Layout(limits layout.Limits) layout.Size { + return layout.Atomic(limits, layout.Fill, layout.Fixed(1)) +} func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) { line := d.date diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index f8fc0d3..f7b868e 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -3,6 +3,7 @@ package chat import ( "cmp" "fmt" + "github.com/ayn2op/tview/layout" "log/slog" "time" @@ -351,6 +352,14 @@ type paneElement struct { child tview.Element } +func (p paneElement) Size() (width, height layout.Length) { + return p.child.Size() +} + +func (p paneElement) Layout(limits layout.Limits) layout.Size { + return p.child.Layout(limits) +} + func (p paneElement) Draw(screen tview.Screen, area tview.Rectangle) { p.child.Draw(screen, area) } @@ -419,7 +428,7 @@ func (m Model) View() tview.Element { var right tview.Element = column.New( m.paneView(paneMessagesList), - column.New(m.paneView(paneComposer)).Height(tview.Fixed(m.composer.Height())), + column.New(m.paneView(paneComposer)).Height(layout.Fixed(m.composer.Height())), ) if mentions := m.composer.MentionsView(); mentions != nil { right = stack.New(right, mentions) @@ -429,15 +438,15 @@ func (m Model) View() tview.Element { if m.membersTreeShown() { width := m.cfg.MembersTree.WidthPercent main = row.New( - column.New(main).Width(tview.FillPortion(100-width)), - column.New(m.paneView(paneMembersTree)).Width(tview.FillPortion(width)), + column.New(main).Width(layout.FillPortion(100-width)), + column.New(m.paneView(paneMembersTree)).Width(layout.FillPortion(width)), ) } if m.guildsTreeVisible { width := m.cfg.Sidebar.WidthPercent main = row.New( - column.New(m.paneView(paneGuildsTree)).Width(tview.FillPortion(width)), - column.New(main).Width(tview.FillPortion(100-width)), + column.New(m.paneView(paneGuildsTree)).Width(layout.FillPortion(width)), + column.New(main).Width(layout.FillPortion(100-width)), ) } @@ -448,7 +457,7 @@ func (m Model) View() tview.Element { // The panes behind the picker take no input. inert.New(main), backdrop.New().Style(m.cfg.Theme.Dialog.BackgroundStyle.Style), - center.New(column.New(picker).Width(tview.Fixed(m.cfg.Picker.Width)).Height(tview.Fixed(m.cfg.Picker.Height))), + center.New(column.New(picker).Width(layout.Fixed(m.cfg.Picker.Width)).Height(layout.Fixed(m.cfg.Picker.Height))), ) } diff --git a/internal/ui/login/password/model.go b/internal/ui/login/password/model.go index f49e98d..ce9fa49 100644 --- a/internal/ui/login/password/model.go +++ b/internal/ui/login/password/model.go @@ -4,6 +4,7 @@ import ( "github.com/ayn2op/tview" "github.com/ayn2op/tview/button" "github.com/ayn2op/tview/column" + "github.com/ayn2op/tview/layout" "github.com/ayn2op/tview/row" "github.com/ayn2op/tview/text" "github.com/ayn2op/tview/textinput" @@ -60,7 +61,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { } func (m Model) View() tview.Element { - labelWidth := tview.Fixed(len("Password") + 1) + labelWidth := layout.Fixed(len("Password") + 1) login := textinput.New(&m.login). Focused(m.focus == focusLogin). OnChange(func(c textinput.Change) tview.Msg { return loginMsg(c) }). @@ -72,13 +73,13 @@ func (m Model) View() tview.Element { OnSubmit(focusMsg(focusButton)) submit := button.New(). Label("Login"). - Width(tview.Fixed(len("Login") + 4)). - Height(tview.Fixed(1)). + Width(layout.Fixed(len("Login") + 4)). + Height(layout.Fixed(1)). Focused(m.focus == focusButton). OnClick(submitMsg{}) return column.New( - row.New(column.New(text.New("Login")).Width(labelWidth), login).Height(tview.Fixed(1)), - row.New(column.New(text.New("Password")).Width(labelWidth), password).Height(tview.Fixed(1)), + row.New(column.New(text.New("Login")).Width(labelWidth), login).Height(layout.Fixed(1)), + row.New(column.New(text.New("Password")).Width(labelWidth), password).Height(layout.Fixed(1)), submit, ).Spacing(1) } diff --git a/internal/ui/login/qr/model.go b/internal/ui/login/qr/model.go index 8b24c38..fefad8f 100644 --- a/internal/ui/login/qr/model.go +++ b/internal/ui/login/qr/model.go @@ -2,6 +2,7 @@ package qr import ( "crypto/rsa" + "github.com/ayn2op/tview/layout" "strings" "time" @@ -140,7 +141,7 @@ func (m Model) View() tview.Element { ScrollState(&m.scrollState). Wrap(false). Alignment(tview.AlignmentCenter). - Height(tview.Fixed(len(m.code))). + Height(layout.Fixed(len(m.code))). Focused(true). OnChange(func(a textview.Change) tview.Msg { return scrollMsg(a) }), ) diff --git a/internal/ui/login/token/model.go b/internal/ui/login/token/model.go index 842db94..a2f03b9 100644 --- a/internal/ui/login/token/model.go +++ b/internal/ui/login/token/model.go @@ -4,6 +4,7 @@ import ( "github.com/ayn2op/tview" "github.com/ayn2op/tview/button" "github.com/ayn2op/tview/column" + "github.com/ayn2op/tview/layout" "github.com/ayn2op/tview/row" "github.com/ayn2op/tview/text" "github.com/ayn2op/tview/textinput" @@ -53,12 +54,12 @@ func (m Model) View() tview.Element { OnSubmit(focusButtonMsg{}) submit := button.New(). Label("Login"). - Width(tview.Fixed(len("Login") + 4)). - Height(tview.Fixed(1)). + Width(layout.Fixed(len("Login") + 4)). + Height(layout.Fixed(1)). Focused(m.buttonFocused). OnClick(submitMsg{}) return column.New( - row.New(column.New(text.New("Token")).Width(tview.Fixed(len("Token")+1)), input).Height(tview.Fixed(1)), + row.New(column.New(text.New("Token")).Width(layout.Fixed(len("Token")+1)), input).Height(layout.Fixed(1)), submit, ).Spacing(1) } diff --git a/internal/ui/root/model.go b/internal/ui/root/model.go index 925a4af..7fce5ef 100644 --- a/internal/ui/root/model.go +++ b/internal/ui/root/model.go @@ -1,6 +1,7 @@ package root import ( + "github.com/ayn2op/tview/layout" "os" "github.com/ayn2op/discordo/internal/config" @@ -144,6 +145,11 @@ func (m *Model) updateScreen(msg tview.Msg) tview.Cmd { return cmd } +// helpHeight returns the rows the help takes at an unlimited width, as the width of the screen is not known in View. +func (m Model) helpHeight() int { + return m.helpView().Layout(layout.Limits{Infinite: layout.Axes{Height: true}}).Height +} + // View shows the inner model above help, with the modal dialog on top of both when one is open. func (m Model) View() tview.Element { var innerView, helpView tview.Element @@ -155,7 +161,7 @@ func (m Model) View() tview.Element { } if m.helpVisible { padded := box.New(m.helpView()).Padding(0, 0, m.cfg.Help.Padding[0], m.cfg.Help.Padding[1]) - helpView = column.New(padded).Height(tview.Fixed(max(m.helpView().Rows(0), 1))) + helpView = column.New(padded).Height(layout.Fixed(max(m.helpHeight(), 1))) } content := column.New(innerView, helpView) if m.modal == nil { diff --git a/internal/ui/root/model_test.go b/internal/ui/root/model_test.go index 3dde073..a4a3f1c 100644 --- a/internal/ui/root/model_test.go +++ b/internal/ui/root/model_test.go @@ -21,7 +21,7 @@ func TestNewModel(t *testing.T) { if !m.helpVisible { t.Fatal("help is hidden") } - if height := m.helpView().Rows(0); height != 1 { + if height := m.helpHeight(); height != 1 { t.Fatalf("height = %d, want 1", height) } diff --git a/internal/ui/tree/tree.go b/internal/ui/tree/tree.go index a7a81ae..92ed13f 100644 --- a/internal/ui/tree/tree.go +++ b/internal/ui/tree/tree.go @@ -12,7 +12,7 @@ import ( ) // New returns a boxed tree of the nodes under root, drawn with theme and moved with kbs, that turns changes and selected nodes into the messages onChange and onSelect return. -func New(root *tree.Node, selectionState *tree.SelectionState, cfg *config.Config, theme config.CommonTreeTheme, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget { +func New(root *tree.Node, selectionState tree.SelectionState, cfg *config.Config, theme config.CommonTreeTheme, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget { set := cfg.Theme.Border.NormalSet.BorderSet if focused { set = cfg.Theme.Border.ActiveSet.BorderSet