From 8aa134b8b23cf945f5a18e21e5fa4855e188d3c0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Mar 2019 00:37:35 +0200 Subject: [PATCH] Start moving to mauview --- go.mod | 3 +- go.sum | 18 +- ui/fuzzy-search-modal.go | 61 ++-- ui/message-view.go | 137 ++++---- ui/messages/imagemessage.go | 2 +- ui/messages/parser/parser.go | 24 +- ui/messages/tstring/cell.go | 3 +- ui/messages/tstring/string.go | 3 +- ui/room-list.go | 75 +++-- ui/room-view.go | 177 ++++++---- ui/tag-room-list.go | 46 ++- ui/ui.go | 47 ++- ui/view-login.go | 89 +++-- ui/view-main.go | 239 +++++++------ ui/widget/advanced-inputfield.go | 562 ------------------------------- ui/widget/border.go | 33 +- ui/widget/center.go | 70 ---- ui/widget/form-text-view.go | 44 --- ui/widget/util.go | 31 +- 19 files changed, 554 insertions(+), 1110 deletions(-) delete mode 100644 ui/widget/advanced-inputfield.go delete mode 100644 ui/widget/center.go delete mode 100644 ui/widget/form-text-view.go diff --git a/go.mod b/go.mod index 1934c0d..4f6a8c7 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,8 @@ require ( github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b golang.org/x/net v0.0.0-20190110200230-915654e7eabc - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 maunium.net/go/mautrix v0.1.0-alpha.3 + maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686 maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009 - maunium.net/go/tview v0.0.0-20190111223510-de38190b095b ) diff --git a/go.sum b/go.sum index 98f259c..f1bc936 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC1 github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs= +github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -18,29 +20,17 @@ github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8 github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM= golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -maunium.net/go/mautrix v0.1.0-alpha.2 h1:NsLc5tyrp5tyrKTvFSmqcLi+FISQ+FsuWC/ycL08PzI= -maunium.net/go/mautrix v0.1.0-alpha.2/go.mod h1:C8akEpHpmmO8gQhLvmInr3HujhUXyKvCoCAzFsxHjGE= maunium.net/go/mautrix v0.1.0-alpha.3 h1:kBz7M63hRetQnAnYK+gVmuSxsmZesX6xERphVgEn324= maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI= -maunium.net/go/tcell v0.0.0-20190111210611-542340841245 h1:JIekRWZ4na6cZJa5VMwLFpuiGzmeX+5Xx+WRVOHQHuk= -maunium.net/go/tcell v0.0.0-20190111210611-542340841245/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM= -maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9 h1:aSZPhcBmGu8MIddPxWNksfjbXHmlRL0yUddwB9CP4s0= -maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM= +maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686 h1:kFgijToFPbMQGIMElizZGPQsffu+ZqO0olORXnfj1g4= +maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686/go.mod h1:Uw1CaNoCs9id/rKBF3Eg9KhhFVg+3akJTebZomFKW+4= maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009 h1:4lojuJmNSun1nUB67m3DGg+RkYg1MUO6aUxgKQU5iZk= maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM= -maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0 h1:xHG0S9ExKp+6dkhasnK/fgO9mLHSSSqVoAymjyUtDdI= -maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0/go.mod h1:ypYT6Dn71E7sVv6NxCjNo2cBJWJa257VSHCGOssGbV0= -maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a h1:f4JVX4GHJH/wMcL9VACMVXT0eaWhxx9a7OT/NLcxsw8= -maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a/go.mod h1:CTOF8OnDeK31Wl25GXdbYzTDvvZoiazmKNZdwkVUmzE= -maunium.net/go/tview v0.0.0-20190111223510-de38190b095b h1:misvyPolT0TVGAtjc9Lr+oEYqGsV4YDByJVqZHxuu70= -maunium.net/go/tview v0.0.0-20190111223510-de38190b095b/go.mod h1:Oi2eW32B8/cE7ZYXL6jyHMrXJL8ARDOQk/3aDvLEyVs= diff --git a/ui/fuzzy-search-modal.go b/ui/fuzzy-search-modal.go index 995b4ba..774a804 100644 --- a/ui/fuzzy-search-modal.go +++ b/ui/fuzzy-search-modal.go @@ -23,19 +23,18 @@ import ( "github.com/lithammer/fuzzysearch/fuzzy" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/widget" ) type FuzzySearchModal struct { - tview.Primitive + mauview.Component - search *tview.InputField - results *tview.TextView + search *mauview.InputField + results *mauview.TextView matches fuzzy.Ranks selected int @@ -43,39 +42,39 @@ type FuzzySearchModal struct { roomList []*rooms.Room roomTitles []string - parent *GomuksUI - mainView *MainView + parent *MainView } func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal { fs := &FuzzySearchModal{ - parent: mainView.parent, - mainView: mainView, + parent: mainView, } fs.InitList(mainView.rooms) - fs.search = tview.NewInputField(). - SetLabel("Room: ") - fs.search. - SetChangedFunc(fs.changeHandler). - SetInputCapture(fs.keyHandler) + fs.search = mauview.NewInputField().SetChangedFunc(fs.changeHandler) + wrappedSearch := mauview.NewBox(fs.search).SetKeyCaptureFunc(fs.keyHandler) + searchLabel := mauview.NewTextField().SetText("Room") + combinedSearch := mauview.NewFlex(). + SetDirection(mauview.FlexColumn). + AddFixedComponent(searchLabel, 5). + AddProportionalComponent(wrappedSearch, 1) - fs.results = tview.NewTextView(). - SetRegions(true) - fs.results.SetBorderPadding(1, 0, 0, 0) + fs.results = mauview.NewTextView().SetRegions(true) // Flex widget containing input box and results - container := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(fs.search, 1, 0, true). - AddItem(fs.results, 0, 1, false) - container. + container := mauview.NewBox(mauview.NewFlex(). + SetDirection(mauview.FlexRow). + AddFixedComponent(combinedSearch, 1). + AddProportionalComponent(fs.results, 1)). SetBorder(true). - SetBorderPadding(1, 1, 1, 1). - SetTitle("Quick Room Switcher") + SetTitle("Quick Room Switcher"). + SetBlurCaptureFunc(func() bool { + fs.parent.HideModal() + return true + }) - fs.Primitive = widget.TransparentCenter(width, height, container) + fs.Component = mauview.Center(container, width, height) return fs } @@ -96,7 +95,7 @@ func (fs *FuzzySearchModal) changeHandler(str string) { for _, match := range fs.matches { fmt.Fprintf(fs.results, `["%d"]%s[""]%s`, match.OriginalIndex, match.Target, "\n") } - fs.parent.Render() + fs.parent.parent.Render() fs.results.Highlight(strconv.Itoa(fs.matches[0].OriginalIndex)) fs.results.ScrollToBeginning() } else { @@ -105,13 +104,12 @@ func (fs *FuzzySearchModal) changeHandler(str string) { } } -func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey { +func (fs *FuzzySearchModal) keyHandler(event mauview.KeyEvent) mauview.KeyEvent { highlights := fs.results.GetHighlights() switch event.Key() { case tcell.KeyEsc: // Close room finder - fs.parent.views.RemovePage("fuzzy-search-modal") - fs.parent.app.SetFocus(fs.parent.views) + fs.parent.HideModal() return nil case tcell.KeyTab: // Cycle highlighted area to next match @@ -125,10 +123,9 @@ func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey { // Switch room to currently selected room if len(highlights) > 0 { debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle()) - fs.mainView.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex]) + fs.parent.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex]) } - fs.parent.views.RemovePage("fuzzy-search-modal") - fs.parent.app.SetFocus(fs.parent.views) + fs.parent.HideModal() fs.results.Clear() fs.search.SetText("") return nil diff --git a/ui/message-view.go b/ui/message-view.go index 4af0ba9..c8a7bfd 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -25,8 +25,8 @@ import ( "github.com/mattn/go-runewidth" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" @@ -38,8 +38,6 @@ import ( ) type MessageView struct { - *tview.Box - parent *RoomView config *config.Config @@ -51,6 +49,8 @@ type MessageView struct { LoadingMessages bool widestSender int + width int + height int prevWidth int prevHeight int prevMsgCount int @@ -65,7 +65,6 @@ type MessageView struct { func NewMessageView(parent *RoomView) *MessageView { return &MessageView{ - Box: tview.NewBox(), parent: parent, config: parent.config, @@ -174,7 +173,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag view.updateWidestSender(message.Sender()) - _, _, width, _ := view.GetRect() + width := view.width bare := view.config.Preferences.BareMessageView if !bare { width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap @@ -267,15 +266,15 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages } func (view *MessageView) recalculateBuffers() { - _, _, width, height := view.GetRect() prefs := view.config.Preferences - if !prefs.BareMessageView { - width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap - } - recalculateMessageBuffers := width != view.prevWidth || + recalculateMessageBuffers := view.width != view.prevWidth || view.prevPrefs.BareMessageView != prefs.BareMessageView || view.prevPrefs.DisableImages != prefs.DisableImages if recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { + width := view.width + if !prefs.BareMessageView { + width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap + } view.textBuffer = []tstring.TString{} view.metaBuffer = []ifc.MessageMeta{} view.prevMsgCount = 0 @@ -290,8 +289,8 @@ func (view *MessageView) recalculateBuffers() { view.appendBuffer(message) } } - view.prevHeight = height - view.prevWidth = width + view.prevHeight = view.height + view.prevWidth = view.width view.prevPrefs = prefs } @@ -343,50 +342,56 @@ func (view *MessageView) handleUsernameClick(message ifc.MessageMeta, prevMessag return true } -func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool { - if button != tcell.Button1 { - return false +func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool { + switch event.Buttons() { + case tcell.WheelUp: + if view.IsAtTop() { + go view.parent.parent.LoadHistory(view.parent.Room.ID) + } else { + view.AddScrollOffset(WheelScrollOffsetDiff) + return true + } + case tcell.WheelDown: + view.AddScrollOffset(-WheelScrollOffsetDiff) + view.parent.parent.MarkRead(view.parent) + return true + case tcell.Button1: + x, y := event.Position() + line := view.TotalHeight() - view.ScrollOffset - view.height + y + if line < 0 || line >= view.TotalHeight() { + return false + } + + message := view.metaBuffer[line] + var prevMessage ifc.MessageMeta + if y != 0 && line > 0 { + prevMessage = view.metaBuffer[line-1] + } + + usernameX := view.TimestampWidth + TimestampSenderGap + messageX := usernameX + view.widestSender + SenderMessageGap + + if x >= messageX { + return view.handleMessageClick(message) + } else if x >= usernameX { + return view.handleUsernameClick(message, prevMessage) + } } - - _, _, _, height := view.GetRect() - line := view.TotalHeight() - view.ScrollOffset - height + y - if line < 0 || line >= view.TotalHeight() { - return false - } - - message := view.metaBuffer[line] - var prevMessage ifc.MessageMeta - if y != 0 && line > 0 { - prevMessage = view.metaBuffer[line-1] - } - - usernameX := view.TimestampWidth + TimestampSenderGap - messageX := usernameX + view.widestSender + SenderMessageGap - - shouldRerender := false - if x >= messageX { - shouldRerender = view.handleMessageClick(message) - } else if x >= usernameX { - shouldRerender = view.handleUsernameClick(message, prevMessage) - } - - return shouldRerender + return false } const PaddingAtTop = 5 func (view *MessageView) AddScrollOffset(diff int) { - _, _, _, height := view.GetRect() - totalHeight := view.TotalHeight() - if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop { - view.ScrollOffset = totalHeight - height + PaddingAtTop + if diff >= 0 && view.ScrollOffset+diff >= totalHeight-view.height+PaddingAtTop { + view.ScrollOffset = totalHeight - view.height + PaddingAtTop } else { view.ScrollOffset += diff } - if view.ScrollOffset > totalHeight-height+PaddingAtTop { - view.ScrollOffset = totalHeight - height + PaddingAtTop + if view.ScrollOffset > totalHeight-view.height+PaddingAtTop { + view.ScrollOffset = totalHeight - view.height + PaddingAtTop } if view.ScrollOffset < 0 { view.ScrollOffset = 0 @@ -394,8 +399,7 @@ func (view *MessageView) AddScrollOffset(diff int) { } func (view *MessageView) Height() int { - _, _, _, height := view.GetRect() - return height + return view.height } func (view *MessageView) TotalHeight() int { @@ -403,9 +407,8 @@ func (view *MessageView) TotalHeight() int { } func (view *MessageView) IsAtTop() bool { - _, _, _, height := view.GetRect() totalHeight := len(view.textBuffer) - return view.ScrollOffset >= totalHeight-height+PaddingAtTop + return view.ScrollOffset >= totalHeight-view.height+PaddingAtTop } const ( @@ -449,15 +452,14 @@ func (view *MessageView) calculateScrollBar(height int) (scrollBarHeight, scroll return } -func (view *MessageView) getIndexOffset(screen tcell.Screen, height, messageX int) (indexOffset int) { +func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX int) (indexOffset int) { indexOffset = view.TotalHeight() - view.ScrollOffset - height if indexOffset <= -PaddingAtTop { message := "Scroll up to load more messages." if view.LoadingMessages { message = "Loading more messages..." } - _, y, _, _ := view.GetRect() - widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen) + widget.WriteLineSimpleColor(screen, message, messageX, 0, tcell.ColorGreen) } return } @@ -488,25 +490,25 @@ func (view *MessageView) CapturePlaintext(height int) string { return buf.String() } -func (view *MessageView) Draw(screen tcell.Screen) { - x, y, _, height := view.GetRect() +func (view *MessageView) Draw(screen mauview.Screen) { + view.width, view.height = screen.Size() view.recalculateBuffers() if view.TotalHeight() == 0 { - widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height) + widget.WriteLineSimple(screen, "It's quite empty in here.", 0, view.height) return } - usernameX := x + view.TimestampWidth + TimestampSenderGap + usernameX := view.TimestampWidth + TimestampSenderGap messageX := usernameX + view.widestSender + SenderMessageGap separatorX := usernameX + view.widestSender + SenderSeparatorGap bareMode := view.config.Preferences.BareMessageView if bareMode { - messageX = x + messageX = 0 } - indexOffset := view.getIndexOffset(screen, height, messageX) + indexOffset := view.getIndexOffset(screen, view.height, messageX) if len(view.textBuffer) != len(view.metaBuffer) { debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer)) @@ -514,13 +516,13 @@ func (view *MessageView) Draw(screen tcell.Screen) { return } - scrollBarHeight, scrollBarPos := view.calculateScrollBar(height) + scrollBarHeight, scrollBarPos := view.calculateScrollBar(view.height) var prevMeta ifc.MessageMeta firstLine := true skippedLines := 0 - for line := 0; line < height; line++ { + for line := 0; line < view.height; line++ { index := indexOffset + line if index < 0 { skippedLines++ @@ -530,31 +532,32 @@ func (view *MessageView) Draw(screen tcell.Screen) { } showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos - isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight() - isBottom := line == height-1 && view.ScrollOffset == 0 + isTop := firstLine && view.ScrollOffset+view.height >= view.TotalHeight() + isBottom := line == view.height-1 && view.ScrollOffset == 0 borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom) firstLine = false if !bareMode { - screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle) + screen.SetContent(separatorX, line, borderChar, nil, borderStyle) } text, meta := view.textBuffer[index], view.metaBuffer[index] if meta != prevMeta { if len(meta.FormatTime()) > 0 { - widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor()) + widget.WriteLineSimpleColor(screen, meta.FormatTime(), 0, line, meta.TimestampColor()) } if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) { widget.WriteLineColor( - screen, tview.AlignRight, meta.Sender(), - usernameX, y+line, view.widestSender, + screen, mauview.AlignRight, meta.Sender(), + usernameX, line, view.widestSender, meta.SenderColor()) } prevMeta = meta } - text.Draw(screen, messageX, y+line) + text.Draw(screen, messageX, line) } + debug.Print(screen) } diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index 0efe676..cad76a4 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -80,7 +80,7 @@ func (msg *ImageMessage) updateData() { debug.Print("Loading image:", msg.Homeserver, msg.FileID) data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) if err != nil { - debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err) + debug.Printf("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err) return } debug.Print("Image", msg.Homeserver, msg.FileID, "loaded.") diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go index 94ab5b6..b36fde0 100644 --- a/ui/messages/parser/parser.go +++ b/ui/messages/parser/parser.go @@ -102,14 +102,24 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve } if len(evt.Content.GetReplyTo()) > 0 { evt.Content.RemoveReplyFallback() - replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo()) - replyToEvt.Content.RemoveReplyFallback() - if len(replyToEvt.Content.FormattedBody) == 0 { - replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) + roomID := evt.Content.RelatesTo.InReplyTo.RoomID + if len(roomID) == 0 { + roomID = room.ID + } + replyToEvt, _ := matrix.Client().GetEvent(roomID, evt.Content.GetReplyTo()) + if replyToEvt != nil { + replyToEvt.Content.RemoveReplyFallback() + if len(replyToEvt.Content.FormattedBody) == 0 { + replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) + } + evt.Content.FormattedBody = fmt.Sprintf( + "In reply to %[1]s
%[2]s

%[3]s", + replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) + } else { + evt.Content.FormattedBody = fmt.Sprintf( + "In reply to unknown event https://matrix.to/#/%[1]s/%[2]s
%[3]s", + roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody) } - evt.Content.FormattedBody = fmt.Sprintf( - "In reply to %[1]s
%[2]s

%[3]s", - replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) } ts := unixToTime(evt.Timestamp) switch evt.Content.MsgType { diff --git a/ui/messages/tstring/cell.go b/ui/messages/tstring/cell.go index aee1716..3ea7b5a 100644 --- a/ui/messages/tstring/cell.go +++ b/ui/messages/tstring/cell.go @@ -18,6 +18,7 @@ package tstring import ( "github.com/mattn/go-runewidth" + "maunium.net/go/mauview" "maunium.net/go/tcell" ) @@ -43,7 +44,7 @@ func (cell Cell) RuneWidth() int { return runewidth.RuneWidth(cell.Char) } -func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) { +func (cell Cell) Draw(screen mauview.Screen, x, y int) (chWidth int) { chWidth = cell.RuneWidth() for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ { screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style) diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index 7feeda0..b14dc8e 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -21,6 +21,7 @@ import ( "unicode" "github.com/mattn/go-runewidth" + "maunium.net/go/mauview" "maunium.net/go/tcell" ) @@ -181,7 +182,7 @@ func (str TString) AdjustStyleFull(fn func(tcell.Style) tcell.Style) { str.AdjustStyle(0, len(str), fn) } -func (str TString) Draw(screen tcell.Screen, x, y int) { +func (str TString) Draw(screen mauview.Screen, x, y int) { offsetX := 0 for _, cell := range str { offsetX += cell.Draw(screen, x+offsetX, y) diff --git a/ui/room-list.go b/ui/room-list.go index a3ae17e..259e315 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -21,15 +21,15 @@ import ( "regexp" "strings" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/matrix/rooms" ) type RoomList struct { - *tview.Box + parent *MainView // The list of tags in display order. tags []string @@ -40,6 +40,8 @@ type RoomList struct { selectedTag string scrollOffset int + height int + width int // The item main text color. mainTextColor tcell.Color @@ -49,9 +51,10 @@ type RoomList struct { selectedBackgroundColor tcell.Color } -func NewRoomList() *RoomList { +func NewRoomList(parent *MainView) *RoomList { list := &RoomList{ - Box: tview.NewBox(), + parent: parent, + items: make(map[string]*TagRoomList), tags: []string{"m.favourite", "net.maunium.gomuks.fake.direct", "", "m.lowpriority"}, @@ -182,11 +185,10 @@ func (list *RoomList) SetSelected(tag string, room *rooms.Room) { list.selected = room list.selectedTag = tag pos := list.index(tag, room) - _, _, _, height := list.GetRect() if pos <= list.scrollOffset { list.scrollOffset = pos - 1 - } else if pos >= list.scrollOffset+height { - list.scrollOffset = pos - height + 1 + } else if pos >= list.scrollOffset+list.height { + list.scrollOffset = pos - list.height + 1 } if list.scrollOffset < 0 { list.scrollOffset = 0 @@ -208,10 +210,9 @@ func (list *RoomList) SelectedRoom() *rooms.Room { func (list *RoomList) AddScrollOffset(offset int) { list.scrollOffset += offset - _, _, _, viewHeight := list.GetRect() contentHeight := list.ContentHeight() - if list.scrollOffset > contentHeight-viewHeight { - list.scrollOffset = contentHeight - viewHeight + if list.scrollOffset > contentHeight-list.height { + list.scrollOffset = contentHeight - list.height } if list.scrollOffset < 0 { list.scrollOffset = 0 @@ -372,10 +373,39 @@ func (list *RoomList) ContentHeight() (height int) { return } -func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Room) { +func (list *RoomList) OnKeyEvent(event mauview.KeyEvent) bool { + return false +} + +func (list *RoomList) OnPasteEvent(event mauview.PasteEvent) bool { + return false +} + +func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool { + switch event.Buttons() { + case tcell.WheelUp: + list.AddScrollOffset(-WheelScrollOffsetDiff) + case tcell.WheelDown: + list.AddScrollOffset(WheelScrollOffsetDiff) + case tcell.Button1: + x, y := event.Position() + return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl) + } + return false +} + +func (list *RoomList) Focus() { + +} + +func (list *RoomList) Blur() { + +} + +func (list *RoomList) clickRoom(line, column int, mod bool) bool { line += list.scrollOffset if line < 0 { - return "", nil + return false } for _, tag := range list.tags { trl := list.items[tag] @@ -391,7 +421,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro if line < 0 { break } else if line < trl.Length() { - return tag, trl.Visible()[trl.Length()-1-line].Room + list.parent.SwitchRoom(tag, trl.Visible()[trl.Length()-1-line].Room) + return true } // Tag items @@ -405,10 +436,9 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro if mod { diff = 100 } - _, _, width, _ := list.GetRect() if column <= 6 && hasLess { trl.maxShown -= diff - } else if column >= width-6 && hasMore { + } else if column >= list.width-6 && hasMore { trl.maxShown += diff } if trl.maxShown < 10 { @@ -420,9 +450,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro // Tag footer line-- } - return "", nil + return false } - var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$") func (list *RoomList) GetTagDisplayName(tag string) string { @@ -445,11 +474,10 @@ func (list *RoomList) GetTagDisplayName(tag string) string { } // Draw draws this primitive onto the screen. -func (list *RoomList) Draw(screen tcell.Screen) { - list.Box.Draw(screen) - - x, y, width, height := list.GetRect() - yLimit := y + height +func (list *RoomList) Draw(screen mauview.Screen) { + list.width, list.height = screen.Size() + y := 0 + yLimit := y + list.height y -= list.scrollOffset // Draw the list items. @@ -464,8 +492,7 @@ func (list *RoomList) Draw(screen tcell.Screen) { if y+renderHeight >= yLimit { renderHeight = yLimit - y } - trl.SetRect(x, y, width, renderHeight) - trl.Draw(screen) + trl.Draw(mauview.NewProxyScreen(screen, 0, y, list.width, renderHeight)) y += renderHeight if y >= yLimit { break diff --git a/ui/room-view.go b/ui/room-view.go index 67d4e83..2931372 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -26,9 +26,10 @@ import ( "github.com/mattn/go-runewidth" + "maunium.net/go/mauview" + "maunium.net/go/mautrix" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/interface" @@ -39,16 +40,23 @@ import ( ) type RoomView struct { - *tview.Box - - topic *tview.TextView + topic *mauview.TextView content *MessageView - status *tview.TextView - userList *tview.TextView + status *mauview.TextField + userList *mauview.TextView ulBorder *widget.Border - input *widget.AdvancedInputField + input *mauview.InputArea Room *rooms.Room + topicScreen *mauview.ProxyScreen + contentScreen *mauview.ProxyScreen + statusScreen *mauview.ProxyScreen + inputScreen *mauview.ProxyScreen + ulBorderScreen *mauview.ProxyScreen + ulScreen *mauview.ProxyScreen + + prevScreen mauview.Screen + parent *MainView config *config.Config @@ -63,27 +71,34 @@ type RoomView struct { func NewRoomView(parent *MainView, room *rooms.Room) *RoomView { view := &RoomView{ - Box: tview.NewBox(), - topic: tview.NewTextView(), - status: tview.NewTextView(), - userList: tview.NewTextView(), + topic: mauview.NewTextView(), + status: mauview.NewTextField(), + userList: mauview.NewTextView(), ulBorder: widget.NewBorder(), - input: widget.NewAdvancedInputField(), + input: mauview.NewInputArea(), Room: room, + + topicScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: 0, Height: TopicBarHeight}, + contentScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: StatusBarHeight}, + statusScreen: &mauview.ProxyScreen{OffsetX: 0, Height: StatusBarHeight}, + inputScreen: &mauview.ProxyScreen{OffsetX: 0}, + ulBorderScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListBorderWidth}, + ulScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListWidth}, + parent: parent, config: parent.config, } view.content = NewMessageView(view) view.input. - SetFieldBackgroundColor(tcell.ColorDefault). + SetBackgroundColor(tcell.ColorDefault). SetPlaceholder("Send a message..."). - SetPlaceholderExtColor(tcell.ColorGray). + SetPlaceholderTextColor(tcell.ColorGray). SetTabCompleteFunc(view.InputTabComplete) view.topic. SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). - SetBackgroundColor(tcell.ColorDarkGreen) + SetTextColor(tcell.ColorDarkGreen) view.status.SetBackgroundColor(tcell.ColorDimGray) @@ -106,26 +121,13 @@ func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int, return view.MessageView().LoadHistory(matrix, view.logPath(dir)) } -func (view *RoomView) SetInputCapture(fn func(room *RoomView, event *tcell.EventKey) *tcell.EventKey) *RoomView { - view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - return fn(view, event) - }) - return view -} - -func (view *RoomView) SetMouseCapture(fn func(room *RoomView, event *tcell.EventMouse) *tcell.EventMouse) *RoomView { - view.input.SetMouseCapture(func(event *tcell.EventMouse) *tcell.EventMouse { - return fn(view, event) - }) - return view -} - func (view *RoomView) SetInputSubmitFunc(fn func(room *RoomView, text string)) *RoomView { - view.input.SetDoneFunc(func(key tcell.Key) { + // FIXME + /*view.input.SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEnter { fn(view, view.input.GetText()) } - }) + })*/ return view } @@ -137,7 +139,7 @@ func (view *RoomView) SetInputChangedFunc(fn func(room *RoomView, text string)) } func (view *RoomView) SetInputText(newText string) *RoomView { - view.input.SetText(newText) + view.input.SetTextAndMoveCursor(newText) return view } @@ -145,12 +147,12 @@ func (view *RoomView) GetInputText() string { return view.input.GetText() } -func (view *RoomView) GetInputField() *widget.AdvancedInputField { - return view.input +func (view *RoomView) Focus() { + view.input.Focus() } -func (view *RoomView) Focus(delegate func(p tview.Primitive)) { - delegate(view.input) +func (view *RoomView) Blur() { + view.input.Blur() } func (view *RoomView) GetStatus() string { @@ -169,7 +171,7 @@ func (view *RoomView) GetStatus() string { buf.WriteString("Typing: " + view.typing[0]) buf.WriteString(" - ") } else if len(view.typing) > 1 { - fmt.Fprintf(&buf, + _, _ = fmt.Fprintf(&buf, "Typing: %s and %s - ", strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1]) } @@ -177,64 +179,91 @@ func (view *RoomView) GetStatus() string { return strings.TrimSuffix(buf.String(), " - ") } -func (view *RoomView) Draw(screen tcell.Screen) { - x, y, width, height := view.GetRect() +// Constants defining the size of the room view grid. +const ( + UserListBorderWidth = 1 + UserListWidth = 20 + StaticHorizontalSpace = UserListBorderWidth + UserListWidth + + TopicBarHeight = 1 + StatusBarHeight = 1 + InputBarHeight = 1 + StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight + + MaxInputHeight +) + +func (view *RoomView) Draw(screen mauview.Screen) { + width, height := screen.Size() if width <= 0 || height <= 0 { return } - // Constants defining the size of the room view grid. - const ( - UserListBorderWidth = 1 - UserListWidth = 20 - StaticHorizontalSpace = UserListBorderWidth + UserListWidth - - TopicBarHeight = 1 - StatusBarHeight = 1 - InputBarHeight = 1 - StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight - ) - // Calculate actual grid based on view rectangle and constants defined above. var ( contentHeight = height - StaticVerticalSpace contentWidth = width - StaticHorizontalSpace - - userListBorderColumn = x + contentWidth - userListColumn = userListBorderColumn + UserListBorderWidth - - topicRow = y - contentRow = topicRow + TopicBarHeight - statusRow = contentRow + contentHeight - inputRow = statusRow + StatusBarHeight ) if view.config.Preferences.HideUserList { contentWidth = width } - // Update the rectangles of all the children. - view.topic.SetRect(x, topicRow, width, TopicBarHeight) - view.content.SetRect(x, contentRow, contentWidth, contentHeight) - view.status.SetRect(x, statusRow, width, StatusBarHeight) - if !view.config.Preferences.HideUserList && userListColumn > x { - view.userList.SetRect(userListColumn, contentRow, UserListWidth, contentHeight) - view.ulBorder.SetRect(userListBorderColumn, contentRow, UserListBorderWidth, contentHeight) + if view.prevScreen != screen { + view.topicScreen.Parent = screen + view.contentScreen.Parent = screen + view.statusScreen.Parent = screen + view.inputScreen.Parent = screen + view.ulBorderScreen.Parent = screen + view.ulScreen.Parent = screen + view.prevScreen = screen } - view.input.SetRect(x, inputRow, width, InputBarHeight) + + view.input.PrepareDraw(width) + inputHeight := view.input.GetTextHeight() + if inputHeight > MaxInputHeight { + inputHeight = MaxInputHeight + } else if inputHeight < 1 { + inputHeight = 1 + } + contentHeight -= inputHeight + + view.topicScreen.Width = width + view.contentScreen.Width = contentWidth + view.contentScreen.Height = contentHeight + view.statusScreen.OffsetY = view.contentScreen.YEnd() + view.statusScreen.Width = width + view.inputScreen.Width = width + view.inputScreen.OffsetY = view.statusScreen.YEnd() + view.inputScreen.Height = inputHeight + view.ulBorderScreen.OffsetX = view.contentScreen.XEnd() + view.ulBorderScreen.Height = contentHeight + view.ulScreen.OffsetX = view.ulBorderScreen.XEnd() + view.ulScreen.Height = contentHeight // Draw everything - view.Box.Draw(screen) - view.topic.Draw(screen) - view.content.Draw(screen) + view.topic.Draw(view.topicScreen) + view.content.Draw(view.contentScreen) view.status.SetText(view.GetStatus()) - view.status.Draw(screen) - view.input.Draw(screen) + view.status.Draw(view.statusScreen) + view.input.Draw(view.inputScreen) if !view.config.Preferences.HideUserList { - view.ulBorder.Draw(screen) - view.userList.Draw(screen) + view.ulBorder.Draw(view.ulBorderScreen) + view.userList.Draw(view.ulScreen) } } +func (view *RoomView) OnKeyEvent(event mauview.KeyEvent) bool { + return view.input.OnKeyEvent(event) +} + +func (view *RoomView) OnPasteEvent(event mauview.PasteEvent) bool { + return view.input.OnPasteEvent(event) +} + +func (view *RoomView) OnMouseEvent(event mauview.MouseEvent) bool { + return view.content.OnMouseEvent(event) +} + func (view *RoomView) SetCompletions(completions []string) { view.completions.list = completions view.completions.textCache = view.input.GetText() diff --git a/ui/tag-room-list.go b/ui/tag-room-list.go index 3c30914..c965f10 100644 --- a/ui/tag-room-list.go +++ b/ui/tag-room-list.go @@ -21,8 +21,8 @@ import ( "strconv" "strings" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/widget" @@ -44,7 +44,7 @@ func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom { return NewOrderedRoom("0.5", room) } -func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineWidth int, isSelected bool) { +func (or *OrderedRoom) Draw(roomList *RoomList, screen mauview.Screen, x, y, lineWidth int, isSelected bool) { style := tcell.StyleDefault. Foreground(roomList.mainTextColor). Bold(or.HasNewMessages()) @@ -56,7 +56,7 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW unreadCount := or.UnreadCount() - widget.WriteLinePadded(screen, tview.AlignLeft, or.GetTitle(), x, y, lineWidth, style) + widget.WriteLinePadded(screen, mauview.AlignLeft, or.GetTitle(), x, y, lineWidth, style) if unreadCount > 0 { unreadMessageCount := "99+" @@ -67,13 +67,13 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW unreadMessageCount += "!" } unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount) - widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style) + widget.WriteLine(screen, mauview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style) lineWidth -= len(unreadMessageCount) } } type TagRoomList struct { - *tview.Box + mauview.NoopEventHandler rooms []*OrderedRoom maxShown int name string @@ -83,7 +83,6 @@ type TagRoomList struct { func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList { return &TagRoomList{ - Box: tview.NewBox(), maxShown: 10, rooms: rooms, name: name, @@ -246,41 +245,40 @@ func (trl *TagRoomList) RenderHeight() int { return height } -func (trl *TagRoomList) DrawHeader(screen tcell.Screen) { - x, y, width, _ := trl.GetRect() +func (trl *TagRoomList) DrawHeader(screen mauview.Screen) { + width, _ := screen.Size() roomCount := strconv.Itoa(trl.TotalLength()) // Draw tag name displayNameWidth := width - 1 - len(roomCount) - widget.WriteLine(screen, tview.AlignLeft, trl.displayname, x, y, displayNameWidth, TagDisplayNameStyle) + widget.WriteLine(screen, mauview.AlignLeft, trl.displayname, 0, 0, displayNameWidth, TagDisplayNameStyle) // Draw tag room count - roomCountX := x + len(trl.displayname) + 1 + roomCountX := len(trl.displayname) + 1 roomCountWidth := width - 2 - len(trl.displayname) - widget.WriteLine(screen, tview.AlignLeft, roomCount, roomCountX, y, roomCountWidth, TagRoomCountStyle) + widget.WriteLine(screen, mauview.AlignLeft, roomCount, roomCountX, 0, roomCountWidth, TagRoomCountStyle) } -func (trl *TagRoomList) Draw(screen tcell.Screen) { +func (trl *TagRoomList) Draw(screen mauview.Screen) { if len(trl.displayname) == 0 { return } trl.DrawHeader(screen) - x, y, width, height := trl.GetRect() - yLimit := y + height + width, height := screen.Size() items := trl.Visible() if trl.IsCollapsed() { - screen.SetCell(x+width-1, y, tcell.StyleDefault, '▶') + screen.SetCell(width-1, 0, tcell.StyleDefault, '▶') return } - screen.SetCell(x+width-1, y, tcell.StyleDefault, '▼') + screen.SetCell(width-1, 0, tcell.StyleDefault, '▼') - offsetY := 1 + y := 1 for i := trl.Length() - 1; i >= 0; i-- { - if y+offsetY >= yLimit { + if y >= height { return } @@ -288,18 +286,18 @@ func (trl *TagRoomList) Draw(screen tcell.Screen) { lineWidth := width isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected - item.Draw(trl.parent, screen, x, y+offsetY, lineWidth, isSelected) - offsetY++ + item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected) + y++ } hasLess := trl.maxShown > 10 hasMore := trl.HasInvisibleRooms() - if (hasLess || hasMore) && y+offsetY < yLimit { + if (hasLess || hasMore) && y < height { if hasMore { - widget.WriteLine(screen, tview.AlignRight, "More ↓", x, y+offsetY, width, tcell.StyleDefault) + widget.WriteLine(screen, mauview.AlignRight, "More ↓", 0, y, width, tcell.StyleDefault) } if hasLess { - widget.WriteLine(screen, tview.AlignLeft, "↑ Less", x, y+offsetY, width, tcell.StyleDefault) + widget.WriteLine(screen, mauview.AlignLeft, "↑ Less", 0, y, width, tcell.StyleDefault) } - offsetY++ + y++ } } diff --git a/ui/ui.go b/ui/ui.go index 5188278..66a3524 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -19,8 +19,8 @@ package ui import ( "os" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/interface" ) @@ -34,17 +34,18 @@ const ( ) type GomuksUI struct { - gmx ifc.Gomuks - app *tview.Application - views *tview.Pages + gmx ifc.Gomuks + app *mauview.Application mainView *MainView loginView *LoginView + + views map[View]mauview.Component } func init() { - tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault - tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen + mauview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault + mauview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 { if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() { os.Setenv("TCELLDB", "/usr/share/tcell/database") @@ -54,20 +55,22 @@ func init() { func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI { ui := &GomuksUI{ - gmx: gmx, - app: tview.NewApplication(), - views: tview.NewPages(), + gmx: gmx, + app: mauview.NewApplication(), } - ui.views.SetChangedFunc(ui.Render) return ui } func (ui *GomuksUI) Init() { - ui.app.SetRoot(ui.InitViews(), true) + ui.views = map[View]mauview.Component{ + ViewLogin: ui.NewLoginView(), + ViewMain: ui.NewMainView(), + } + ui.app.Root = ui.views[ViewLogin] } func (ui *GomuksUI) Start() error { - return ui.app.Run() + return ui.app.Start() } func (ui *GomuksUI) Stop() { @@ -75,23 +78,21 @@ func (ui *GomuksUI) Stop() { } func (ui *GomuksUI) Finish() { - if ui.app.GetScreen() != nil { - ui.app.GetScreen().Fini() + if ui.app.Screen() != nil { + ui.app.Screen().Fini() } } func (ui *GomuksUI) Render() { - ui.app.Draw() + ui.app.Redraw() } func (ui *GomuksUI) OnLogin() { ui.SetView(ViewMain) - ui.app.SetFocus(ui.mainView) } func (ui *GomuksUI) OnLogout() { ui.SetView(ViewLogin) - ui.app.SetFocus(ui.loginView) } func (ui *GomuksUI) HandleNewPreferences() { @@ -99,13 +100,11 @@ func (ui *GomuksUI) HandleNewPreferences() { } func (ui *GomuksUI) SetView(name View) { - ui.views.SwitchToPage(string(name)) -} - -func (ui *GomuksUI) InitViews() tview.Primitive { - ui.views.AddPage(string(ViewLogin), ui.NewLoginView(), true, true) - ui.views.AddPage(string(ViewMain), ui.NewMainView(), true, false) - return ui.views + ui.app.Root = ui.views[name] + focusable, ok := ui.app.Root.(mauview.Focusable) + if ok { + focusable.Focus() + } } func (ui *GomuksUI) MainView() ifc.MainView { diff --git a/ui/view-login.go b/ui/view-login.go index 603899c..1cef96f 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -17,66 +17,97 @@ package ui import ( + "maunium.net/go/tcell" + "maunium.net/go/mautrix" - "maunium.net/go/tview" + "maunium.net/go/mauview" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/ui/widget" ) type LoginView struct { - *tview.Form + *mauview.Form - homeserver *widget.AdvancedInputField - username *widget.AdvancedInputField - password *widget.AdvancedInputField - error *widget.FormTextView + container *mauview.Centerer + + homeserverLabel *mauview.TextField + usernameLabel *mauview.TextField + passwordLabel *mauview.TextField + + homeserver *mauview.InputField + username *mauview.InputField + password *mauview.InputField + error *mauview.TextField + + loginButton *mauview.Button + quitButton *mauview.Button matrix ifc.MatrixContainer config *config.Config parent *GomuksUI } -func (ui *GomuksUI) NewLoginView() tview.Primitive { +func (ui *GomuksUI) NewLoginView() mauview.Component { view := &LoginView{ - Form: tview.NewForm(), + Form: mauview.NewForm(), - homeserver: widget.NewAdvancedInputField(), - username: widget.NewAdvancedInputField(), - password: widget.NewAdvancedInputField(), + usernameLabel: mauview.NewTextField().SetText("Username"), + passwordLabel: mauview.NewTextField().SetText("Password"), + homeserverLabel: mauview.NewTextField().SetText("Homeserver"), + + username: mauview.NewInputField(), + password: mauview.NewInputField(), + homeserver: mauview.NewInputField(), + + loginButton: mauview.NewButton("Login"), + quitButton: mauview.NewButton("Quit"), matrix: ui.gmx.Matrix(), config: ui.gmx.Config(), parent: ui, } + hs := ui.gmx.Config().HS - if len(hs) == 0 { - hs = "https://matrix.org" - } - view.homeserver.SetLabel("Homeserver").SetText(hs).SetFieldWidth(30) - view.username.SetLabel("Username").SetText(ui.gmx.Config().UserID).SetFieldWidth(30) - view.password.SetLabel("Password").SetMaskCharacter('*').SetFieldWidth(30) + view.homeserver.SetText(hs) + view.username.SetText(ui.gmx.Config().UserID) + view.password.SetMaskCharacter('*') - view. - AddFormItem(view.homeserver).AddFormItem(view.username).AddFormItem(view.password). - AddButton("Log in", view.Login). - AddButton("Quit", ui.gmx.Stop). - SetButtonsAlign(tview.AlignCenter). - SetBorder(true).SetTitle("Log in to Matrix") + view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorBlue) + view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorBlue) + view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1}) + view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) + view.AddFormItem(view.username, 3, 1, 5, 1). + AddFormItem(view.password, 3, 3, 5, 1). + AddFormItem(view.homeserver, 3, 5, 5, 1). + AddFormItem(view.loginButton, 5, 7, 3, 1). + AddFormItem(view.quitButton, 1, 7, 3, 1). + AddComponent(view.usernameLabel, 1, 1, 1, 1). + AddComponent(view.passwordLabel, 1, 3, 1, 1). + AddComponent(view.homeserverLabel, 1, 5, 1, 1) ui.loginView = view - return widget.Center(45, 13, ui.loginView) + view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 45, 13) + return view.container } func (view *LoginView) Error(err string) { + if len(err) == 0 { + debug.Print("Hiding error") + view.RemoveComponent(view.error) + view.error = nil + return + } + debug.Print("Showing error", err) if view.error == nil { - view.error = &widget.FormTextView{TextView: tview.NewTextView()} - view.AddFormItem(view.error) + view.error = mauview.NewTextField().SetTextColor(tcell.ColorRed) + view.AddComponent(view.error, 1, 9, 7, 1) } view.error.SetText(err) + + view.parent.Render() } func (view *LoginView) Login() { @@ -91,8 +122,8 @@ func (view *LoginView) Login() { err = view.matrix.Login(mxid, password) if err != nil { if httpErr, ok := err.(mautrix.HTTPError); ok { - if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { - view.Error(respErr.Err) + if httpErr.RespError != nil { + view.Error(httpErr.RespError.Err) } else { view.Error(httpErr.Message) } diff --git a/ui/view-main.go b/ui/view-main.go index 254dc9f..8b2d895 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -26,8 +26,8 @@ import ( "github.com/kyokomi/emoji" "maunium.net/go/mautrix" + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" @@ -40,12 +40,16 @@ import ( ) type MainView struct { - *tview.Flex + flex *mauview.Flex roomList *RoomList - roomView *tview.Pages + roomView *mauview.Box + currentRoom *RoomView rooms map[string]*RoomView cmdProcessor *CommandProcessor + focused mauview.Focusable + + modal mauview.Component lastFocusTime time.Time @@ -55,11 +59,10 @@ type MainView struct { parent *GomuksUI } -func (ui *GomuksUI) NewMainView() tview.Primitive { +func (ui *GomuksUI) NewMainView() mauview.Component { mainView := &MainView{ - Flex: tview.NewFlex(), - roomList: NewRoomList(), - roomView: tview.NewPages(), + flex: mauview.NewFlex().SetDirection(mauview.FlexColumn), + roomView: mauview.NewBox(nil).SetBorder(false), rooms: make(map[string]*RoomView), matrix: ui.gmx.Matrix(), @@ -67,13 +70,13 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { config: ui.gmx.Config(), parent: ui, } + mainView.roomList = NewRoomList(mainView) mainView.cmdProcessor = NewCommandProcessor(mainView) - mainView. - SetDirection(tview.FlexColumn). - AddItem(mainView.roomList, 25, 0, false). - AddItem(widget.NewBorder(), 1, 0, false). - AddItem(mainView.roomView, 0, 1, true) + mainView.flex. + AddFixedComponent(mainView.roomList, 25). + AddFixedComponent(widget.NewBorder(), 1). + AddProportionalComponent(mainView.roomView, 1) mainView.BumpFocus(nil) ui.mainView = mainView @@ -81,18 +84,37 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { return mainView } -func (view *MainView) Draw(screen tcell.Screen) { +func (view *MainView) ShowModal(modal mauview.Component) { + view.modal = modal + var ok bool + view.focused, ok = modal.(mauview.Focusable) + if !ok { + view.focused = nil + } +} + +func (view *MainView) HideModal() { + view.modal = nil + view.focused = view.roomView +} + +func (view *MainView) Draw(screen mauview.Screen) { if view.config.Preferences.HideRoomList { - view.roomView.SetRect(view.GetRect()) view.roomView.Draw(screen) } else { - view.Flex.Draw(screen) + view.flex.Draw(screen) + } + + if view.modal != nil { + view.modal.Draw(screen) } } func (view *MainView) BumpFocus(roomView *RoomView) { - view.lastFocusTime = time.Now() - view.MarkRead(roomView) + if roomView != nil { + view.lastFocusTime = time.Now() + view.MarkRead(roomView) + } } func (view *MainView) MarkRead(roomView *RoomView) { @@ -167,7 +189,10 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag } func (view *MainView) ShowBare(roomView *RoomView) { - _, height := view.parent.app.GetScreen().Size() + if roomView == nil { + return + } + _, height := view.parent.app.Screen().Size() view.parent.app.Suspend(func() { print("\033[2J\033[0;0H") // We don't know how much space there exactly is. Too few messages looks weird, @@ -181,37 +206,54 @@ func (view *MainView) ShowBare(roomView *RoomView) { }) } -func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey { - view.BumpFocus(roomView) +func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool { + view.BumpFocus(view.currentRoom) - k := key.Key() - c := key.Rune() - if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt { + if view.modal != nil { + return view.modal.OnKeyEvent(event) + } + + k := event.Key() + c := event.Rune() + if event.Modifiers() == tcell.ModCtrl || event.Modifiers() == tcell.ModAlt { switch { case k == tcell.KeyDown: view.SwitchRoom(view.roomList.Next()) case k == tcell.KeyUp: view.SwitchRoom(view.roomList.Previous()) case k == tcell.KeyEnter: - searchModal := NewFuzzySearchModal(view, 42, 12) - view.parent.views.AddPage("fuzzy-search-modal", searchModal, true, true) - view.parent.app.SetFocus(searchModal) + view.ShowModal(NewFuzzySearchModal(view, 42, 12)) + case k == tcell.KeyHome: + msgView := view.currentRoom.MessageView() + msgView.AddScrollOffset(msgView.TotalHeight()) + case k == tcell.KeyEnd: + msgView := view.currentRoom.MessageView() + msgView.AddScrollOffset(-msgView.TotalHeight()) + case k == tcell.KeyCtrlN: + return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers())) case c == 'a': view.SwitchRoom(view.roomList.NextWithActivity()) case c == 'l': - view.ShowBare(roomView) + view.ShowBare(view.currentRoom) default: - return key + goto defaultHandler } + return true } else if k == tcell.KeyAltDown || k == tcell.KeyCtrlDown { view.SwitchRoom(view.roomList.Next()) + return true } else if k == tcell.KeyAltUp || k == tcell.KeyCtrlUp { view.SwitchRoom(view.roomList.Previous()) - } else if k == tcell.KeyPgUp || k == tcell.KeyPgDn || k == tcell.KeyUp || k == tcell.KeyDown || k == tcell.KeyEnd || k == tcell.KeyHome { - msgView := roomView.MessageView() + return true + } else if view.currentRoom != nil && + (k == tcell.KeyPgUp || k == tcell.KeyPgDn || + k == tcell.KeyUp || k == tcell.KeyDown || + k == tcell.KeyEnd || k == tcell.KeyHome) { + // TODO these should be in the RoomView key handler + msgView := view.currentRoom.MessageView() if msgView.IsAtTop() && (k == tcell.KeyPgUp || k == tcell.KeyUp) { - go view.LoadHistory(roomView.Room.ID) + go view.LoadHistory(view.currentRoom.Room.ID) } switch k { @@ -219,80 +261,72 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) * msgView.AddScrollOffset(msgView.Height() / 2) case tcell.KeyPgDn: msgView.AddScrollOffset(-msgView.Height() / 2) - case tcell.KeyUp: - msgView.AddScrollOffset(1) - case tcell.KeyDown: - msgView.AddScrollOffset(-1) - case tcell.KeyHome: - msgView.AddScrollOffset(msgView.TotalHeight()) - case tcell.KeyEnd: - msgView.AddScrollOffset(-msgView.TotalHeight()) + default: + goto defaultHandler } - } else { - return key + return true + } else if k == tcell.KeyEnter { + view.InputSubmit(view.currentRoom, view.currentRoom.input.GetText()) + return true } - return nil +defaultHandler: + if view.config.Preferences.HideRoomList { + debug.Print("Key event going to default handler (direct to roomview)", event) + return view.roomView.OnKeyEvent(event) + } + debug.Print("Key event going to default handler (flex)", event) + return view.flex.OnKeyEvent(event) } const WheelScrollOffsetDiff = 3 -func isInArea(x, y int, p tview.Primitive) bool { - rx, ry, rw, rh := p.GetRect() - return x >= rx && y >= ry && x < rx+rw && y < ry+rh -} - -func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse { - if event.Buttons() == tcell.ButtonNone || event.HasMotion() { - return event +func (view *MainView) OnMouseEvent(event mauview.MouseEvent) bool { + if view.config.Preferences.HideRoomList { + return view.roomView.OnMouseEvent(event) + } + return view.flex.OnMouseEvent(event) + /*if event.Buttons() == tcell.ButtonNone || event.HasMotion() { + return false } - view.BumpFocus(roomView) - msgView := roomView.MessageView() + view.BumpFocus(view.currentRoom) + x, y := event.Position() switch { - case isInArea(x, y, msgView): - mx, my, _, _ := msgView.GetRect() - switch event.Buttons() { - case tcell.WheelUp: - if msgView.IsAtTop() { - go view.LoadHistory(roomView.Room.ID) - } else { - msgView.AddScrollOffset(WheelScrollOffsetDiff) - - view.parent.Render() - } - case tcell.WheelDown: - msgView.AddScrollOffset(-WheelScrollOffsetDiff) - view.parent.Render() - view.MarkRead(roomView) - default: - if msgView.HandleClick(x-mx, y-my, event.Buttons()) { - view.parent.Render() - } - } - case isInArea(x, y, view.roomList): - switch event.Buttons() { - case tcell.WheelUp: - view.roomList.AddScrollOffset(-WheelScrollOffsetDiff) - view.parent.Render() - case tcell.WheelDown: - view.roomList.AddScrollOffset(WheelScrollOffsetDiff) - view.parent.Render() - case tcell.Button1: - _, rly, _, _ := msgView.GetRect() - line := y - rly + 1 - switchToTag, switchToRoom := view.roomList.HandleClick(x, line, event.Modifiers() == tcell.ModCtrl) - if switchToRoom != nil { - view.SwitchRoom(switchToTag, switchToRoom) - } else { - view.parent.Render() - } - } + case x >= 27: + view.roomView.OnMouseEvent(mauview.OffsetMouseEvent(event, -27, 0)) + view.roomView.Focus() + view.focused = view.roomView + case x <= 25: + view.roomList.OnMouseEvent(event) + view.roomList.Focus() + view.focused = view.roomList default: debug.Print("Unhandled mouse event:", event.Buttons(), event.Modifiers(), x, y) } - return event + return false*/ +} + +func (view *MainView) OnPasteEvent(event mauview.PasteEvent) bool { + if view.modal != nil { + return view.modal.OnPasteEvent(event) + } else if view.config.Preferences.HideRoomList { + return view.roomView.OnPasteEvent(event) + } + return view.flex.OnPasteEvent(event) +} + +func (view *MainView) Focus() { + if view.focused != nil { + view.focused.Focus() + } +} + +func (view *MainView) Blur() { + if view.focused != nil { + view.focused.Blur() + } } func (view *MainView) SwitchRoom(tag string, room *rooms.Room) { @@ -300,29 +334,19 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) { return } - view.roomView.SwitchToPage(room.ID) roomView := view.rooms[room.ID] if roomView == nil { debug.Print("Tried to switch to non-nil room with nil roomView!") debug.Print(tag, room) return } + view.roomView.SetInnerComponent(roomView) + view.currentRoom = roomView view.MarkRead(roomView) view.roomList.SetSelected(tag, room) - view.parent.app.SetFocus(view) view.parent.Render() } -func (view *MainView) Focus(delegate func(p tview.Primitive)) { - room := view.roomList.SelectedRoom() - if room != nil { - roomView, ok := view.rooms[room.ID] - if ok { - delegate(roomView) - } - } -} - func (view *MainView) SaveAllHistory() { for _, room := range view.rooms { err := room.SaveHistory(view.config.HistoryDir) @@ -333,14 +357,11 @@ func (view *MainView) SaveAllHistory() { } func (view *MainView) addRoomPage(room *rooms.Room) { - if !view.roomView.HasPage(room.ID) { + if _, ok := view.rooms[room.ID]; !ok { roomView := NewRoomView(view, room). SetInputSubmitFunc(view.InputSubmit). - SetInputChangedFunc(view.InputChanged). - SetInputCapture(view.KeyEventHandler). - SetMouseCapture(view.MouseEventHandler) + SetInputChangedFunc(view.InputChanged) view.rooms[room.ID] = roomView - view.roomView.AddPage(room.ID, roomView, true, false) roomView.UpdateUserList() _, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir) @@ -387,7 +408,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) { view.roomList.Remove(room) view.SwitchRoom(view.roomList.Selected()) - view.roomView.RemovePage(room.ID) delete(view.rooms, room.ID) view.parent.Render() @@ -395,7 +415,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) { func (view *MainView) SetRooms(rooms map[string]*rooms.Room) { view.roomList.Clear() - view.roomView.Clear() view.rooms = make(map[string]*RoomView) for _, room := range rooms { if room.HasLeft { diff --git a/ui/widget/advanced-inputfield.go b/ui/widget/advanced-inputfield.go deleted file mode 100644 index 741ae3d..0000000 --- a/ui/widget/advanced-inputfield.go +++ /dev/null @@ -1,562 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Based on https://github.com/rivo/tview/blob/master/inputfield.go - -package widget - -import ( - "math" - "regexp" - "strings" - "unicode/utf8" - - "github.com/mattn/go-runewidth" - "github.com/zyedidia/clipboard" - - "maunium.net/go/tcell" - "maunium.net/go/tview" -) - -// AdvancedInputField is a multi-line user-editable text area. -// -// Use SetMaskCharacter() to hide input from onlookers (e.g. for password -// input). -type AdvancedInputField struct { - *tview.Box - - // Cursor position - cursorOffset int - viewOffset int - - // The text that was entered. - text string - - // The text to be displayed before the input area. - label string - - // The text to be displayed in the input area when "text" is empty. - placeholder string - - // The label color. - labelColor tcell.Color - - // The background color of the input area. - fieldBackgroundColor tcell.Color - - // The text color of the input area. - fieldTextColor tcell.Color - - // The text color of the placeholder. - placeholderTextColor tcell.Color - - // The screen width of the label area. A value of 0 means use the width of - // the label text. - labelWidth int - - // The screen width of the input area. A value of 0 means extend as much as - // possible. - fieldWidth int - - // A character to mask entered text (useful for password fields). A value of 0 - // disables masking. - maskCharacter rune - - // Whether or not to enable vim-style keybindings. - vimBindings bool - - // An optional function which may reject the last character that was entered. - accept func(text string, ch rune) bool - - // An optional function which is called when the input has changed. - changed func(text string) - - // An optional function which is called when the user indicated that they - // are done entering text. The key which was pressed is provided (enter, tab, backtab or escape). - done func(tcell.Key) - - // An optional function which is called when the user presses tab. - tabComplete func(text string, cursorOffset int) -} - -// NewAdvancedInputField returns a new input field. -func NewAdvancedInputField() *AdvancedInputField { - return &AdvancedInputField{ - Box: tview.NewBox(), - labelColor: tview.Styles.SecondaryTextColor, - fieldBackgroundColor: tview.Styles.ContrastBackgroundColor, - fieldTextColor: tview.Styles.PrimaryTextColor, - placeholderTextColor: tview.Styles.ContrastSecondaryTextColor, - } -} - -// SetText sets the current text of the input field. -func (field *AdvancedInputField) SetText(text string) *AdvancedInputField { - field.text = text - if field.changed != nil { - field.changed(text) - } - return field -} - -// SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference. -func (field *AdvancedInputField) SetTextAndMoveCursor(text string) *AdvancedInputField { - oldWidth := runewidth.StringWidth(field.text) - field.text = text - newWidth := runewidth.StringWidth(field.text) - if oldWidth != newWidth { - field.cursorOffset += newWidth - oldWidth - } - if field.changed != nil { - field.changed(field.text) - } - return field -} - -// GetText returns the current text of the input field. -func (field *AdvancedInputField) GetText() string { - return field.text -} - -// SetLabel sets the text to be displayed before the input area. -func (field *AdvancedInputField) SetLabel(label string) *AdvancedInputField { - field.label = label - return field -} - -// SetLabelWidth sets the screen width of the label. A value of 0 will cause the -// primitive to use the width of the label string. -func (field *AdvancedInputField) SetLabelWidth(width int) *AdvancedInputField { - field.labelWidth = width - return field -} - -// GetLabel returns the text to be displayed before the input area. -func (field *AdvancedInputField) GetLabel() string { - return field.label -} - -// SetPlaceholder sets the text to be displayed when the input text is empty. -func (field *AdvancedInputField) SetPlaceholder(text string) *AdvancedInputField { - field.placeholder = text - return field -} - -// SetLabelColor sets the color of the label. -func (field *AdvancedInputField) SetLabelColor(color tcell.Color) *AdvancedInputField { - field.labelColor = color - return field -} - -// SetFieldBackgroundColor sets the background color of the input area. -func (field *AdvancedInputField) SetFieldBackgroundColor(color tcell.Color) *AdvancedInputField { - field.fieldBackgroundColor = color - return field -} - -// SetFieldTextColor sets the text color of the input area. -func (field *AdvancedInputField) SetFieldTextColor(color tcell.Color) *AdvancedInputField { - field.fieldTextColor = color - return field -} - -// SetPlaceholderExtColor sets the text color of placeholder text. -func (field *AdvancedInputField) SetPlaceholderExtColor(color tcell.Color) *AdvancedInputField { - field.placeholderTextColor = color - return field -} - -// SetFormAttributes sets attributes shared by all form items. -func (field *AdvancedInputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem { - field.labelWidth = labelWidth - field.labelColor = labelColor - field.SetBackgroundColor(bgColor) - field.fieldTextColor = fieldTextColor - field.fieldBackgroundColor = fieldBgColor - return field -} - -// SetFieldWidth sets the screen width of the input area. A value of 0 means -// extend as much as possible. -func (field *AdvancedInputField) SetFieldWidth(width int) *AdvancedInputField { - field.fieldWidth = width - return field -} - -// GetFieldWidth returns this primitive's field width. -func (field *AdvancedInputField) GetFieldWidth() int { - return field.fieldWidth -} - -// SetMaskCharacter sets a character that masks user input on a screen. A value -// of 0 disables masking. -func (field *AdvancedInputField) SetMaskCharacter(mask rune) *AdvancedInputField { - field.maskCharacter = mask - return field -} - -// SetAcceptanceFunc sets a handler which may reject the last character that was -// entered (by returning false). -// -// This package defines a number of variables Prefixed with AdvancedInputField which may -// be used for common input (e.g. numbers, maximum text length). -func (field *AdvancedInputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *AdvancedInputField { - field.accept = handler - return field -} - -// SetChangedFunc sets a handler which is called whenever the text of the input -// field has changed. It receives the current text (after the change). -func (field *AdvancedInputField) SetChangedFunc(handler func(text string)) *AdvancedInputField { - field.changed = handler - return field -} - -// SetDoneFunc sets a handler which is called when the user is done entering -// text. The callback function is provided with the key that was pressed, which -// is one of the following: -// -// - KeyEnter: Done entering text. -// - KeyEscape: Abort text input. -// - KeyTab: Tab -// - KeyBacktab: Shift + Tab -func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField { - field.done = handler - return field -} - -func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *AdvancedInputField { - field.tabComplete = handler - return field -} - -// SetFinishedFunc calls SetDoneFunc(). -func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem { - return field.SetDoneFunc(handler) -} - -// drawInput calculates the field width and draws the background. -func (field *AdvancedInputField) drawInput(screen tcell.Screen, rightLimit, x, y int) (fieldWidth int) { - fieldWidth = field.fieldWidth - if fieldWidth == 0 { - fieldWidth = math.MaxInt32 - } - if rightLimit-x < fieldWidth { - fieldWidth = rightLimit - x - } - fieldStyle := tcell.StyleDefault.Background(field.fieldBackgroundColor) - for index := 0; index < fieldWidth; index++ { - screen.SetContent(x+index, y, ' ', nil, fieldStyle) - } - return -} - -// prepareText prepares the text to be displayed and recalculates the view and cursor offsets. -func (field *AdvancedInputField) prepareText(screen tcell.Screen, fieldWidth, x, y int) (text string) { - text = field.text - if text == "" && field.placeholder != "" { - tview.Print(screen, field.placeholder, x, y, fieldWidth, tview.AlignLeft, field.placeholderTextColor) - } - - if field.maskCharacter > 0 { - text = strings.Repeat(string(field.maskCharacter), utf8.RuneCountInString(field.text)) - } - textWidth := runewidth.StringWidth(text) - if field.cursorOffset >= textWidth { - fieldWidth-- - } - - if field.cursorOffset < field.viewOffset { - field.viewOffset = field.cursorOffset - } else if field.cursorOffset > field.viewOffset+fieldWidth { - field.viewOffset = field.cursorOffset - fieldWidth - } else if textWidth-field.viewOffset < fieldWidth { - field.viewOffset = textWidth - fieldWidth - } - - if field.viewOffset < 0 { - field.viewOffset = 0 - } - - return -} - -// drawText draws the text and the cursor. -func (field *AdvancedInputField) drawText(screen tcell.Screen, fieldWidth, x, y int, text string) { - runes := []rune(text) - relPos := 0 - for pos := field.viewOffset; pos <= fieldWidth+field.viewOffset && pos < len(runes); pos++ { - ch := runes[pos] - w := runewidth.RuneWidth(ch) - _, _, style, _ := screen.GetContent(x+relPos, y) - style = style.Foreground(field.fieldTextColor) - for w > 0 { - screen.SetContent(x+relPos, y, ch, nil, style) - relPos++ - w-- - } - } - - // Set cursor. - if field.GetFocusable().HasFocus() { - field.setCursor(screen) - } -} - -// Draw draws this primitive onto the screen. -func (field *AdvancedInputField) Draw(screen tcell.Screen) { - field.Box.Draw(screen) - - x, y, width, height := field.GetInnerRect() - rightLimit := x + width - if height < 1 || rightLimit <= x { - return - } - - // Draw label. - if field.labelWidth > 0 { - labelWidth := field.labelWidth - if labelWidth > rightLimit-x { - labelWidth = rightLimit - x - } - tview.Print(screen, field.label, x, y, labelWidth, tview.AlignLeft, field.labelColor) - x += labelWidth - } else { - _, drawnWidth := tview.Print(screen, field.label, x, y, rightLimit-x, tview.AlignLeft, field.labelColor) - x += drawnWidth - } - - fieldWidth := field.drawInput(screen, rightLimit, x, y) - text := field.prepareText(screen, fieldWidth, x, y) - field.drawText(screen, fieldWidth, x, y, text) -} - -func (field *AdvancedInputField) GetCursorOffset() int { - return field.cursorOffset -} - -func (field *AdvancedInputField) SetCursorOffset(offset int) *AdvancedInputField { - if offset < 0 { - offset = 0 - } else { - width := runewidth.StringWidth(field.text) - if offset >= width { - offset = width - } - } - field.cursorOffset = offset - return field -} - -// setCursor sets the cursor position. -func (field *AdvancedInputField) setCursor(screen tcell.Screen) { - x, y, width, _ := field.GetRect() - origX, origY := x, y - rightLimit := x + width - if field.HasBorder() { - x++ - y++ - rightLimit -= 2 - } - labelWidth := field.labelWidth - if labelWidth == 0 { - labelWidth = tview.StringWidth(field.label) - } - x = x + labelWidth + field.cursorOffset - field.viewOffset - if x >= rightLimit { - x = rightLimit - 1 - } else if x < origX { - x = origY - } - screen.ShowCursor(x, y) -} - -var ( - lastWord = regexp.MustCompile(`\S+\s*$`) - firstWord = regexp.MustCompile(`^\s*\S+`) -) - -func SubstringBefore(s string, w int) string { - return runewidth.Truncate(s, w, "") -} - -func (field *AdvancedInputField) TypeRune(ch rune) { - leftPart := SubstringBefore(field.text, field.cursorOffset) - newText := leftPart + string(ch) + field.text[len(leftPart):] - if field.accept != nil { - if !field.accept(newText, ch) { - return - } - } - field.text = newText - field.cursorOffset += runewidth.RuneWidth(ch) -} - -func (field *AdvancedInputField) PasteClipboard() { - clip, _ := clipboard.ReadAll("clipboard") - leftPart := SubstringBefore(field.text, field.cursorOffset) - field.text = leftPart + clip + field.text[len(leftPart):] - field.cursorOffset += runewidth.StringWidth(clip) -} - -func (field *AdvancedInputField) MoveCursorLeft(moveWord bool) { - before := SubstringBefore(field.text, field.cursorOffset) - if moveWord { - found := lastWord.FindString(before) - field.cursorOffset -= runewidth.StringWidth(found) - } else if len(before) > 0 { - beforeRunes := []rune(before) - char := beforeRunes[len(beforeRunes)-1] - field.cursorOffset -= runewidth.RuneWidth(char) - } -} - -func (field *AdvancedInputField) MoveCursorRight(moveWord bool) { - before := SubstringBefore(field.text, field.cursorOffset) - after := field.text[len(before):] - if moveWord { - found := firstWord.FindString(after) - field.cursorOffset += runewidth.StringWidth(found) - } else if len(after) > 0 { - char := []rune(after)[0] - field.cursorOffset += runewidth.RuneWidth(char) - } -} - -func (field *AdvancedInputField) RemoveNextCharacter() { - if field.cursorOffset >= runewidth.StringWidth(field.text) { - return - } - leftPart := SubstringBefore(field.text, field.cursorOffset) - // Take everything after the left part minus the first character. - rightPart := string([]rune(field.text[len(leftPart):])[1:]) - - field.text = leftPart + rightPart -} - -func (field *AdvancedInputField) Clear() { - field.text = "" - field.cursorOffset = 0 -} - -func (field *AdvancedInputField) RemovePreviousWord() { - leftPart := SubstringBefore(field.text, field.cursorOffset) - rightPart := field.text[len(leftPart):] - replacement := lastWord.ReplaceAllString(leftPart, "") - field.text = replacement + rightPart - - field.cursorOffset -= runewidth.StringWidth(leftPart) - runewidth.StringWidth(replacement) -} - -func (field *AdvancedInputField) RemovePreviousCharacter() { - if field.cursorOffset == 0 { - return - } - leftPart := SubstringBefore(field.text, field.cursorOffset) - rightPart := field.text[len(leftPart):] - - // Take everything before the right part minus the last character. - leftPartRunes := []rune(leftPart) - leftPartRunes = leftPartRunes[0 : len(leftPartRunes)-1] - leftPart = string(leftPartRunes) - - // Figure out what character was removed to correctly decrease cursorOffset. - removedChar := field.text[len(leftPart) : len(field.text)-len(rightPart)] - - field.text = leftPart + rightPart - - field.cursorOffset -= runewidth.StringWidth(removedChar) -} - -func (field *AdvancedInputField) TriggerTabComplete() bool { - if field.tabComplete != nil { - field.tabComplete(field.text, field.cursorOffset) - return true - } - return false -} - -func (field *AdvancedInputField) handleInputChanges(originalText string) { - // Trigger changed events. - if field.text != originalText && field.changed != nil { - field.changed(field.text) - } - - // Make sure cursor offset is valid - if field.cursorOffset < 0 { - field.cursorOffset = 0 - } - width := runewidth.StringWidth(field.text) - if field.cursorOffset > width { - field.cursorOffset = width - } -} - -// InputHandler returns the handler for this primitive. -func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return field.WrapInputHandler(field.inputHandler) -} - -func (field *AdvancedInputField) PasteHandler() func(event *tcell.EventPaste) { - return field.WrapPasteHandler(field.pasteHandler) -} - -func (field *AdvancedInputField) pasteHandler(event *tcell.EventPaste) { - defer field.handleInputChanges(field.text) - clip := event.Text() - leftPart := SubstringBefore(field.text, field.cursorOffset) - field.text = leftPart + clip + field.text[len(leftPart):] - field.cursorOffset += runewidth.StringWidth(clip) -} - -func (field *AdvancedInputField) inputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - defer field.handleInputChanges(field.text) - - // Process key event. - switch key := event.Key(); key { - case tcell.KeyRune: - field.TypeRune(event.Rune()) - case tcell.KeyCtrlV: - field.PasteClipboard() - case tcell.KeyLeft: - field.MoveCursorLeft(event.Modifiers() == tcell.ModCtrl) - case tcell.KeyRight: - field.MoveCursorRight(event.Modifiers() == tcell.ModCtrl) - case tcell.KeyDelete: - field.RemoveNextCharacter() - case tcell.KeyCtrlU: - if field.vimBindings { - field.Clear() - } - case tcell.KeyCtrlW: - if field.vimBindings { - field.RemovePreviousWord() - } - case tcell.KeyBackspace: - field.RemovePreviousWord() - case tcell.KeyBackspace2: - field.RemovePreviousCharacter() - case tcell.KeyTab: - if field.TriggerTabComplete() { - break - } - fallthrough - case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyBacktab: - if field.done != nil { - field.done(key) - } - } -} diff --git a/ui/widget/border.go b/ui/widget/border.go index 8ec772d..f7f367f 100644 --- a/ui/widget/border.go +++ b/ui/widget/border.go @@ -17,8 +17,8 @@ package widget import ( + "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/tview" ) // Border is a simple tview widget that renders a horizontal or vertical bar. @@ -27,24 +27,37 @@ import ( // If the height is 1, the bar will be horizontal. // If the width nor the height are 1, nothing will be rendered. type Border struct { - *tview.Box + Style tcell.Style } // NewBorder wraps a new tview Box into a new Border. func NewBorder() *Border { - return &Border{tview.NewBox()} + return &Border{ + Style: tcell.StyleDefault.Foreground(mauview.Styles.BorderColor), + } } -func (border *Border) Draw(screen tcell.Screen) { - background := tcell.StyleDefault.Background(border.GetBackgroundColor()).Foreground(border.GetBorderColor()) - x, y, width, height := border.GetRect() +func (border *Border) Draw(screen mauview.Screen) { + width, height := screen.Size() if width == 1 { - for borderY := y; borderY < y+height; borderY++ { - screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background) + for borderY := 0; borderY < height; borderY++ { + screen.SetContent(0, borderY, mauview.Borders.Vertical, nil, border.Style) } } else if height == 1 { - for borderX := x; borderX < x+width; borderX++ { - screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background) + for borderX := 0; borderX < width; borderX++ { + screen.SetContent(borderX, 0, mauview.Borders.Horizontal, nil, border.Style) } } } + +func (border *Border) OnKeyEvent(event mauview.KeyEvent) bool { + return false +} + +func (border *Border) OnPasteEvent(event mauview.PasteEvent) bool { + return false +} + +func (border *Border) OnMouseEvent(event mauview.MouseEvent) bool { + return false +} diff --git a/ui/widget/center.go b/ui/widget/center.go deleted file mode 100644 index cc994bb..0000000 --- a/ui/widget/center.go +++ /dev/null @@ -1,70 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package widget - -import ( - "maunium.net/go/tcell" - "maunium.net/go/tview" -) - -// Center wraps the given tview primitive into a Flex element in order to -// vertically and horizontally center the given primitive. -func Center(width, height int, p tview.Primitive) tview.Primitive { - return tview.NewFlex(). - AddItem(nil, 0, 1, false). - AddItem(tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(nil, 0, 1, false). - AddItem(p, height, 1, true). - AddItem(nil, 0, 1, false), width, 1, true). - AddItem(nil, 0, 1, false) -} - -type transparentCenter struct { - *tview.Box - prefWidth, prefHeight int - p tview.Primitive -} - -func TransparentCenter(width, height int, p tview.Primitive) tview.Primitive { - return &transparentCenter{ - Box: tview.NewBox(), - prefWidth: width, - prefHeight: height, - p: p, - } -} - -func (tc *transparentCenter) Draw(screen tcell.Screen) { - x, y, width, height := tc.GetRect() - if width > tc.prefWidth { - x += (width - tc.prefWidth) / 2 - width = tc.prefWidth - } - if height > tc.prefHeight { - y += (height - tc.prefHeight) / 2 - height = tc.prefHeight - } - tc.p.SetRect(x, y, width, height) - tc.p.Draw(screen) -} - -func (tc *transparentCenter) Focus(delegate func(p tview.Primitive)) { - if delegate != nil { - delegate(tc.p) - } -} diff --git a/ui/widget/form-text-view.go b/ui/widget/form-text-view.go deleted file mode 100644 index 2a8683c..0000000 --- a/ui/widget/form-text-view.go +++ /dev/null @@ -1,44 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package widget - -import ( - "maunium.net/go/tcell" - "maunium.net/go/tview" -) - -type FormTextView struct { - *tview.TextView -} - -func (ftv *FormTextView) GetLabel() string { - return "" -} - -func (ftv *FormTextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem { - return ftv -} - -func (ftv *FormTextView) GetFieldWidth() int { - _, _, w, _ := ftv.TextView.GetRect() - return w -} - -func (ftv *FormTextView) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem { - ftv.SetDoneFunc(handler) - return ftv -} diff --git a/ui/widget/util.go b/ui/widget/util.go index ed51735..6a2173d 100644 --- a/ui/widget/util.go +++ b/ui/widget/util.go @@ -18,28 +18,31 @@ package widget import ( "fmt" - "github.com/mattn/go-runewidth" - "maunium.net/go/tcell" - "maunium.net/go/tview" "strconv" + + "github.com/mattn/go-runewidth" + + "maunium.net/go/mauview" + "maunium.net/go/tcell" ) -func WriteLineSimple(screen tcell.Screen, line string, x, y int) { - WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault) +func WriteLineSimple(screen mauview.Screen, line string, x, y int) { + WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault) } -func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) { - WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color)) +func WriteLineSimpleColor(screen mauview.Screen, line string, x, y int, color tcell.Color) { + WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color)) } -func WriteLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) { +func WriteLineColor(screen mauview.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) { WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color)) } -func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) { +func WriteLine(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) { offsetX := 0 - if align == tview.AlignRight { - offsetX = maxWidth - runewidth.StringWidth(line) + if align == mauview.AlignRight { + // TODO is mauview.StringWidth correct here? + offsetX = maxWidth - mauview.StringWidth(line) } if offsetX < 0 { offsetX = 0 @@ -60,12 +63,12 @@ func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, } } -func WriteLinePadded(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) { +func WriteLinePadded(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) { padding := strconv.Itoa(maxWidth) - if align == tview.AlignRight { + if align == mauview.AlignRight { line = fmt.Sprintf("%"+padding+"s", line) } else { line = fmt.Sprintf("%-"+padding+"s", line) } - WriteLine(screen, tview.AlignLeft, line, x, y, maxWidth, style) + WriteLine(screen, mauview.AlignLeft, line, x, y, maxWidth, style) }