Start moving to mauview

This commit is contained in:
Tulir Asokan 2019-03-26 00:37:35 +02:00
parent ae36b9cddd
commit 8aa134b8b2
19 changed files with 554 additions and 1110 deletions

3
go.mod
View File

@ -11,9 +11,8 @@ require (
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b
golang.org/x/net v0.0.0-20190110200230-915654e7eabc 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 gopkg.in/yaml.v2 v2.2.2
maunium.net/go/mautrix v0.1.0-alpha.3 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/tcell v0.0.0-20190111223412-5e74142cb009
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b
) )

18
go.sum
View File

@ -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/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 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= 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= 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 h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 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 h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/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 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 h1:kBz7M63hRetQnAnYK+gVmuSxsmZesX6xERphVgEn324=
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI= 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/mauview v0.0.0-20190325223341-4c387be4b686 h1:kFgijToFPbMQGIMElizZGPQsffu+ZqO0olORXnfj1g4=
maunium.net/go/tcell v0.0.0-20190111210611-542340841245/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM= maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686/go.mod h1:Uw1CaNoCs9id/rKBF3Eg9KhhFVg+3akJTebZomFKW+4=
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/tcell v0.0.0-20190111223412-5e74142cb009 h1:4lojuJmNSun1nUB67m3DGg+RkYg1MUO6aUxgKQU5iZk= 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/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=

View File

@ -23,19 +23,18 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/widget"
) )
type FuzzySearchModal struct { type FuzzySearchModal struct {
tview.Primitive mauview.Component
search *tview.InputField search *mauview.InputField
results *tview.TextView results *mauview.TextView
matches fuzzy.Ranks matches fuzzy.Ranks
selected int selected int
@ -43,39 +42,39 @@ type FuzzySearchModal struct {
roomList []*rooms.Room roomList []*rooms.Room
roomTitles []string roomTitles []string
parent *GomuksUI parent *MainView
mainView *MainView
} }
func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal { func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal {
fs := &FuzzySearchModal{ fs := &FuzzySearchModal{
parent: mainView.parent, parent: mainView,
mainView: mainView,
} }
fs.InitList(mainView.rooms) fs.InitList(mainView.rooms)
fs.search = tview.NewInputField(). fs.search = mauview.NewInputField().SetChangedFunc(fs.changeHandler)
SetLabel("Room: ") wrappedSearch := mauview.NewBox(fs.search).SetKeyCaptureFunc(fs.keyHandler)
fs.search. searchLabel := mauview.NewTextField().SetText("Room")
SetChangedFunc(fs.changeHandler). combinedSearch := mauview.NewFlex().
SetInputCapture(fs.keyHandler) SetDirection(mauview.FlexColumn).
AddFixedComponent(searchLabel, 5).
AddProportionalComponent(wrappedSearch, 1)
fs.results = tview.NewTextView(). fs.results = mauview.NewTextView().SetRegions(true)
SetRegions(true)
fs.results.SetBorderPadding(1, 0, 0, 0)
// Flex widget containing input box and results // Flex widget containing input box and results
container := tview.NewFlex(). container := mauview.NewBox(mauview.NewFlex().
SetDirection(tview.FlexRow). SetDirection(mauview.FlexRow).
AddItem(fs.search, 1, 0, true). AddFixedComponent(combinedSearch, 1).
AddItem(fs.results, 0, 1, false) AddProportionalComponent(fs.results, 1)).
container.
SetBorder(true). 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 return fs
} }
@ -96,7 +95,7 @@ func (fs *FuzzySearchModal) changeHandler(str string) {
for _, match := range fs.matches { for _, match := range fs.matches {
fmt.Fprintf(fs.results, `["%d"]%s[""]%s`, match.OriginalIndex, match.Target, "\n") 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.Highlight(strconv.Itoa(fs.matches[0].OriginalIndex))
fs.results.ScrollToBeginning() fs.results.ScrollToBeginning()
} else { } 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() highlights := fs.results.GetHighlights()
switch event.Key() { switch event.Key() {
case tcell.KeyEsc: case tcell.KeyEsc:
// Close room finder // Close room finder
fs.parent.views.RemovePage("fuzzy-search-modal") fs.parent.HideModal()
fs.parent.app.SetFocus(fs.parent.views)
return nil return nil
case tcell.KeyTab: case tcell.KeyTab:
// Cycle highlighted area to next match // 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 // Switch room to currently selected room
if len(highlights) > 0 { if len(highlights) > 0 {
debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle()) 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.HideModal()
fs.parent.app.SetFocus(fs.parent.views)
fs.results.Clear() fs.results.Clear()
fs.search.SetText("") fs.search.SetText("")
return nil return nil

View File

@ -25,8 +25,8 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
@ -38,8 +38,6 @@ import (
) )
type MessageView struct { type MessageView struct {
*tview.Box
parent *RoomView parent *RoomView
config *config.Config config *config.Config
@ -51,6 +49,8 @@ type MessageView struct {
LoadingMessages bool LoadingMessages bool
widestSender int widestSender int
width int
height int
prevWidth int prevWidth int
prevHeight int prevHeight int
prevMsgCount int prevMsgCount int
@ -65,7 +65,6 @@ type MessageView struct {
func NewMessageView(parent *RoomView) *MessageView { func NewMessageView(parent *RoomView) *MessageView {
return &MessageView{ return &MessageView{
Box: tview.NewBox(),
parent: parent, parent: parent,
config: parent.config, config: parent.config,
@ -174,7 +173,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
view.updateWidestSender(message.Sender()) view.updateWidestSender(message.Sender())
_, _, width, _ := view.GetRect() width := view.width
bare := view.config.Preferences.BareMessageView bare := view.config.Preferences.BareMessageView
if !bare { if !bare {
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
@ -267,15 +266,15 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
} }
func (view *MessageView) recalculateBuffers() { func (view *MessageView) recalculateBuffers() {
_, _, width, height := view.GetRect()
prefs := view.config.Preferences prefs := view.config.Preferences
if !prefs.BareMessageView { recalculateMessageBuffers := view.width != view.prevWidth ||
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
}
recalculateMessageBuffers := width != view.prevWidth ||
view.prevPrefs.BareMessageView != prefs.BareMessageView || view.prevPrefs.BareMessageView != prefs.BareMessageView ||
view.prevPrefs.DisableImages != prefs.DisableImages view.prevPrefs.DisableImages != prefs.DisableImages
if recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { 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.textBuffer = []tstring.TString{}
view.metaBuffer = []ifc.MessageMeta{} view.metaBuffer = []ifc.MessageMeta{}
view.prevMsgCount = 0 view.prevMsgCount = 0
@ -290,8 +289,8 @@ func (view *MessageView) recalculateBuffers() {
view.appendBuffer(message) view.appendBuffer(message)
} }
} }
view.prevHeight = height view.prevHeight = view.height
view.prevWidth = width view.prevWidth = view.width
view.prevPrefs = prefs view.prevPrefs = prefs
} }
@ -343,50 +342,56 @@ func (view *MessageView) handleUsernameClick(message ifc.MessageMeta, prevMessag
return true return true
} }
func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool { func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
if button != tcell.Button1 { switch event.Buttons() {
return false 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)
}
} }
return false
_, _, _, 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
} }
const PaddingAtTop = 5 const PaddingAtTop = 5
func (view *MessageView) AddScrollOffset(diff int) { func (view *MessageView) AddScrollOffset(diff int) {
_, _, _, height := view.GetRect()
totalHeight := view.TotalHeight() totalHeight := view.TotalHeight()
if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop { if diff >= 0 && view.ScrollOffset+diff >= totalHeight-view.height+PaddingAtTop {
view.ScrollOffset = totalHeight - height + PaddingAtTop view.ScrollOffset = totalHeight - view.height + PaddingAtTop
} else { } else {
view.ScrollOffset += diff view.ScrollOffset += diff
} }
if view.ScrollOffset > totalHeight-height+PaddingAtTop { if view.ScrollOffset > totalHeight-view.height+PaddingAtTop {
view.ScrollOffset = totalHeight - height + PaddingAtTop view.ScrollOffset = totalHeight - view.height + PaddingAtTop
} }
if view.ScrollOffset < 0 { if view.ScrollOffset < 0 {
view.ScrollOffset = 0 view.ScrollOffset = 0
@ -394,8 +399,7 @@ func (view *MessageView) AddScrollOffset(diff int) {
} }
func (view *MessageView) Height() int { func (view *MessageView) Height() int {
_, _, _, height := view.GetRect() return view.height
return height
} }
func (view *MessageView) TotalHeight() int { func (view *MessageView) TotalHeight() int {
@ -403,9 +407,8 @@ func (view *MessageView) TotalHeight() int {
} }
func (view *MessageView) IsAtTop() bool { func (view *MessageView) IsAtTop() bool {
_, _, _, height := view.GetRect()
totalHeight := len(view.textBuffer) totalHeight := len(view.textBuffer)
return view.ScrollOffset >= totalHeight-height+PaddingAtTop return view.ScrollOffset >= totalHeight-view.height+PaddingAtTop
} }
const ( const (
@ -449,15 +452,14 @@ func (view *MessageView) calculateScrollBar(height int) (scrollBarHeight, scroll
return 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 indexOffset = view.TotalHeight() - view.ScrollOffset - height
if indexOffset <= -PaddingAtTop { if indexOffset <= -PaddingAtTop {
message := "Scroll up to load more messages." message := "Scroll up to load more messages."
if view.LoadingMessages { if view.LoadingMessages {
message = "Loading more messages..." message = "Loading more messages..."
} }
_, y, _, _ := view.GetRect() widget.WriteLineSimpleColor(screen, message, messageX, 0, tcell.ColorGreen)
widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
} }
return return
} }
@ -488,25 +490,25 @@ func (view *MessageView) CapturePlaintext(height int) string {
return buf.String() return buf.String()
} }
func (view *MessageView) Draw(screen tcell.Screen) { func (view *MessageView) Draw(screen mauview.Screen) {
x, y, _, height := view.GetRect() view.width, view.height = screen.Size()
view.recalculateBuffers() view.recalculateBuffers()
if view.TotalHeight() == 0 { 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 return
} }
usernameX := x + view.TimestampWidth + TimestampSenderGap usernameX := view.TimestampWidth + TimestampSenderGap
messageX := usernameX + view.widestSender + SenderMessageGap messageX := usernameX + view.widestSender + SenderMessageGap
separatorX := usernameX + view.widestSender + SenderSeparatorGap separatorX := usernameX + view.widestSender + SenderSeparatorGap
bareMode := view.config.Preferences.BareMessageView bareMode := view.config.Preferences.BareMessageView
if bareMode { 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) { if len(view.textBuffer) != len(view.metaBuffer) {
debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", 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 return
} }
scrollBarHeight, scrollBarPos := view.calculateScrollBar(height) scrollBarHeight, scrollBarPos := view.calculateScrollBar(view.height)
var prevMeta ifc.MessageMeta var prevMeta ifc.MessageMeta
firstLine := true firstLine := true
skippedLines := 0 skippedLines := 0
for line := 0; line < height; line++ { for line := 0; line < view.height; line++ {
index := indexOffset + line index := indexOffset + line
if index < 0 { if index < 0 {
skippedLines++ skippedLines++
@ -530,31 +532,32 @@ func (view *MessageView) Draw(screen tcell.Screen) {
} }
showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight() isTop := firstLine && view.ScrollOffset+view.height >= view.TotalHeight()
isBottom := line == height-1 && view.ScrollOffset == 0 isBottom := line == view.height-1 && view.ScrollOffset == 0
borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom) borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
firstLine = false firstLine = false
if !bareMode { 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] text, meta := view.textBuffer[index], view.metaBuffer[index]
if meta != prevMeta { if meta != prevMeta {
if len(meta.FormatTime()) > 0 { 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()) { if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) {
widget.WriteLineColor( widget.WriteLineColor(
screen, tview.AlignRight, meta.Sender(), screen, mauview.AlignRight, meta.Sender(),
usernameX, y+line, view.widestSender, usernameX, line, view.widestSender,
meta.SenderColor()) meta.SenderColor())
} }
prevMeta = meta prevMeta = meta
} }
text.Draw(screen, messageX, y+line) text.Draw(screen, messageX, line)
} }
debug.Print(screen)
} }

View File

@ -80,7 +80,7 @@ func (msg *ImageMessage) updateData() {
debug.Print("Loading image:", msg.Homeserver, msg.FileID) debug.Print("Loading image:", msg.Homeserver, msg.FileID)
data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
if err != nil { 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 return
} }
debug.Print("Image", msg.Homeserver, msg.FileID, "loaded.") debug.Print("Image", msg.Homeserver, msg.FileID, "loaded.")

View File

@ -102,14 +102,24 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve
} }
if len(evt.Content.GetReplyTo()) > 0 { if len(evt.Content.GetReplyTo()) > 0 {
evt.Content.RemoveReplyFallback() evt.Content.RemoveReplyFallback()
replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo()) roomID := evt.Content.RelatesTo.InReplyTo.RoomID
replyToEvt.Content.RemoveReplyFallback() if len(roomID) == 0 {
if len(replyToEvt.Content.FormattedBody) == 0 { roomID = room.ID
replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) }
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 <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[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<br/>%[3]s",
roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody)
} }
evt.Content.FormattedBody = fmt.Sprintf(
"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
} }
ts := unixToTime(evt.Timestamp) ts := unixToTime(evt.Timestamp)
switch evt.Content.MsgType { switch evt.Content.MsgType {

View File

@ -18,6 +18,7 @@ package tstring
import ( import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
@ -43,7 +44,7 @@ func (cell Cell) RuneWidth() int {
return runewidth.RuneWidth(cell.Char) 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() chWidth = cell.RuneWidth()
for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ { for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style) screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)

View File

@ -21,6 +21,7 @@ import (
"unicode" "unicode"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
@ -181,7 +182,7 @@ func (str TString) AdjustStyleFull(fn func(tcell.Style) tcell.Style) {
str.AdjustStyle(0, len(str), fn) 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 offsetX := 0
for _, cell := range str { for _, cell := range str {
offsetX += cell.Draw(screen, x+offsetX, y) offsetX += cell.Draw(screen, x+offsetX, y)

View File

@ -21,15 +21,15 @@ import (
"regexp" "regexp"
"strings" "strings"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
) )
type RoomList struct { type RoomList struct {
*tview.Box parent *MainView
// The list of tags in display order. // The list of tags in display order.
tags []string tags []string
@ -40,6 +40,8 @@ type RoomList struct {
selectedTag string selectedTag string
scrollOffset int scrollOffset int
height int
width int
// The item main text color. // The item main text color.
mainTextColor tcell.Color mainTextColor tcell.Color
@ -49,9 +51,10 @@ type RoomList struct {
selectedBackgroundColor tcell.Color selectedBackgroundColor tcell.Color
} }
func NewRoomList() *RoomList { func NewRoomList(parent *MainView) *RoomList {
list := &RoomList{ list := &RoomList{
Box: tview.NewBox(), parent: parent,
items: make(map[string]*TagRoomList), items: make(map[string]*TagRoomList),
tags: []string{"m.favourite", "net.maunium.gomuks.fake.direct", "", "m.lowpriority"}, 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.selected = room
list.selectedTag = tag list.selectedTag = tag
pos := list.index(tag, room) pos := list.index(tag, room)
_, _, _, height := list.GetRect()
if pos <= list.scrollOffset { if pos <= list.scrollOffset {
list.scrollOffset = pos - 1 list.scrollOffset = pos - 1
} else if pos >= list.scrollOffset+height { } else if pos >= list.scrollOffset+list.height {
list.scrollOffset = pos - height + 1 list.scrollOffset = pos - list.height + 1
} }
if list.scrollOffset < 0 { if list.scrollOffset < 0 {
list.scrollOffset = 0 list.scrollOffset = 0
@ -208,10 +210,9 @@ func (list *RoomList) SelectedRoom() *rooms.Room {
func (list *RoomList) AddScrollOffset(offset int) { func (list *RoomList) AddScrollOffset(offset int) {
list.scrollOffset += offset list.scrollOffset += offset
_, _, _, viewHeight := list.GetRect()
contentHeight := list.ContentHeight() contentHeight := list.ContentHeight()
if list.scrollOffset > contentHeight-viewHeight { if list.scrollOffset > contentHeight-list.height {
list.scrollOffset = contentHeight - viewHeight list.scrollOffset = contentHeight - list.height
} }
if list.scrollOffset < 0 { if list.scrollOffset < 0 {
list.scrollOffset = 0 list.scrollOffset = 0
@ -372,10 +373,39 @@ func (list *RoomList) ContentHeight() (height int) {
return 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 line += list.scrollOffset
if line < 0 { if line < 0 {
return "", nil return false
} }
for _, tag := range list.tags { for _, tag := range list.tags {
trl := list.items[tag] trl := list.items[tag]
@ -391,7 +421,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
if line < 0 { if line < 0 {
break break
} else if line < trl.Length() { } 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 // Tag items
@ -405,10 +436,9 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
if mod { if mod {
diff = 100 diff = 100
} }
_, _, width, _ := list.GetRect()
if column <= 6 && hasLess { if column <= 6 && hasLess {
trl.maxShown -= diff trl.maxShown -= diff
} else if column >= width-6 && hasMore { } else if column >= list.width-6 && hasMore {
trl.maxShown += diff trl.maxShown += diff
} }
if trl.maxShown < 10 { if trl.maxShown < 10 {
@ -420,9 +450,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
// Tag footer // Tag footer
line-- line--
} }
return "", nil return false
} }
var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$") var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$")
func (list *RoomList) GetTagDisplayName(tag string) string { 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. // Draw draws this primitive onto the screen.
func (list *RoomList) Draw(screen tcell.Screen) { func (list *RoomList) Draw(screen mauview.Screen) {
list.Box.Draw(screen) list.width, list.height = screen.Size()
y := 0
x, y, width, height := list.GetRect() yLimit := y + list.height
yLimit := y + height
y -= list.scrollOffset y -= list.scrollOffset
// Draw the list items. // Draw the list items.
@ -464,8 +492,7 @@ func (list *RoomList) Draw(screen tcell.Screen) {
if y+renderHeight >= yLimit { if y+renderHeight >= yLimit {
renderHeight = yLimit - y renderHeight = yLimit - y
} }
trl.SetRect(x, y, width, renderHeight) trl.Draw(mauview.NewProxyScreen(screen, 0, y, list.width, renderHeight))
trl.Draw(screen)
y += renderHeight y += renderHeight
if y >= yLimit { if y >= yLimit {
break break

View File

@ -26,9 +26,10 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/mauview"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
@ -39,16 +40,23 @@ import (
) )
type RoomView struct { type RoomView struct {
*tview.Box topic *mauview.TextView
topic *tview.TextView
content *MessageView content *MessageView
status *tview.TextView status *mauview.TextField
userList *tview.TextView userList *mauview.TextView
ulBorder *widget.Border ulBorder *widget.Border
input *widget.AdvancedInputField input *mauview.InputArea
Room *rooms.Room 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 parent *MainView
config *config.Config config *config.Config
@ -63,27 +71,34 @@ type RoomView struct {
func NewRoomView(parent *MainView, room *rooms.Room) *RoomView { func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
view := &RoomView{ view := &RoomView{
Box: tview.NewBox(), topic: mauview.NewTextView(),
topic: tview.NewTextView(), status: mauview.NewTextField(),
status: tview.NewTextView(), userList: mauview.NewTextView(),
userList: tview.NewTextView(),
ulBorder: widget.NewBorder(), ulBorder: widget.NewBorder(),
input: widget.NewAdvancedInputField(), input: mauview.NewInputArea(),
Room: room, 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, parent: parent,
config: parent.config, config: parent.config,
} }
view.content = NewMessageView(view) view.content = NewMessageView(view)
view.input. view.input.
SetFieldBackgroundColor(tcell.ColorDefault). SetBackgroundColor(tcell.ColorDefault).
SetPlaceholder("Send a message..."). SetPlaceholder("Send a message...").
SetPlaceholderExtColor(tcell.ColorGray). SetPlaceholderTextColor(tcell.ColorGray).
SetTabCompleteFunc(view.InputTabComplete) SetTabCompleteFunc(view.InputTabComplete)
view.topic. view.topic.
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
SetBackgroundColor(tcell.ColorDarkGreen) SetTextColor(tcell.ColorDarkGreen)
view.status.SetBackgroundColor(tcell.ColorDimGray) 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)) 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 { 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 { if key == tcell.KeyEnter {
fn(view, view.input.GetText()) fn(view, view.input.GetText())
} }
}) })*/
return view return view
} }
@ -137,7 +139,7 @@ func (view *RoomView) SetInputChangedFunc(fn func(room *RoomView, text string))
} }
func (view *RoomView) SetInputText(newText string) *RoomView { func (view *RoomView) SetInputText(newText string) *RoomView {
view.input.SetText(newText) view.input.SetTextAndMoveCursor(newText)
return view return view
} }
@ -145,12 +147,12 @@ func (view *RoomView) GetInputText() string {
return view.input.GetText() return view.input.GetText()
} }
func (view *RoomView) GetInputField() *widget.AdvancedInputField { func (view *RoomView) Focus() {
return view.input view.input.Focus()
} }
func (view *RoomView) Focus(delegate func(p tview.Primitive)) { func (view *RoomView) Blur() {
delegate(view.input) view.input.Blur()
} }
func (view *RoomView) GetStatus() string { func (view *RoomView) GetStatus() string {
@ -169,7 +171,7 @@ func (view *RoomView) GetStatus() string {
buf.WriteString("Typing: " + view.typing[0]) buf.WriteString("Typing: " + view.typing[0])
buf.WriteString(" - ") buf.WriteString(" - ")
} else if len(view.typing) > 1 { } else if len(view.typing) > 1 {
fmt.Fprintf(&buf, _, _ = fmt.Fprintf(&buf,
"Typing: %s and %s - ", "Typing: %s and %s - ",
strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1]) 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(), " - ") return strings.TrimSuffix(buf.String(), " - ")
} }
func (view *RoomView) Draw(screen tcell.Screen) { // Constants defining the size of the room view grid.
x, y, width, height := view.GetRect() 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 { if width <= 0 || height <= 0 {
return 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. // Calculate actual grid based on view rectangle and constants defined above.
var ( var (
contentHeight = height - StaticVerticalSpace contentHeight = height - StaticVerticalSpace
contentWidth = width - StaticHorizontalSpace 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 { if view.config.Preferences.HideUserList {
contentWidth = width contentWidth = width
} }
// Update the rectangles of all the children. if view.prevScreen != screen {
view.topic.SetRect(x, topicRow, width, TopicBarHeight) view.topicScreen.Parent = screen
view.content.SetRect(x, contentRow, contentWidth, contentHeight) view.contentScreen.Parent = screen
view.status.SetRect(x, statusRow, width, StatusBarHeight) view.statusScreen.Parent = screen
if !view.config.Preferences.HideUserList && userListColumn > x { view.inputScreen.Parent = screen
view.userList.SetRect(userListColumn, contentRow, UserListWidth, contentHeight) view.ulBorderScreen.Parent = screen
view.ulBorder.SetRect(userListBorderColumn, contentRow, UserListBorderWidth, contentHeight) 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 // Draw everything
view.Box.Draw(screen) view.topic.Draw(view.topicScreen)
view.topic.Draw(screen) view.content.Draw(view.contentScreen)
view.content.Draw(screen)
view.status.SetText(view.GetStatus()) view.status.SetText(view.GetStatus())
view.status.Draw(screen) view.status.Draw(view.statusScreen)
view.input.Draw(screen) view.input.Draw(view.inputScreen)
if !view.config.Preferences.HideUserList { if !view.config.Preferences.HideUserList {
view.ulBorder.Draw(screen) view.ulBorder.Draw(view.ulBorderScreen)
view.userList.Draw(screen) 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) { func (view *RoomView) SetCompletions(completions []string) {
view.completions.list = completions view.completions.list = completions
view.completions.textCache = view.input.GetText() view.completions.textCache = view.input.GetText()

View File

@ -21,8 +21,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
@ -44,7 +44,7 @@ func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom {
return NewOrderedRoom("0.5", room) 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. style := tcell.StyleDefault.
Foreground(roomList.mainTextColor). Foreground(roomList.mainTextColor).
Bold(or.HasNewMessages()) Bold(or.HasNewMessages())
@ -56,7 +56,7 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
unreadCount := or.UnreadCount() 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 { if unreadCount > 0 {
unreadMessageCount := "99+" unreadMessageCount := "99+"
@ -67,13 +67,13 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
unreadMessageCount += "!" unreadMessageCount += "!"
} }
unreadMessageCount = fmt.Sprintf("(%s)", 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) lineWidth -= len(unreadMessageCount)
} }
} }
type TagRoomList struct { type TagRoomList struct {
*tview.Box mauview.NoopEventHandler
rooms []*OrderedRoom rooms []*OrderedRoom
maxShown int maxShown int
name string name string
@ -83,7 +83,6 @@ type TagRoomList struct {
func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList { func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList {
return &TagRoomList{ return &TagRoomList{
Box: tview.NewBox(),
maxShown: 10, maxShown: 10,
rooms: rooms, rooms: rooms,
name: name, name: name,
@ -246,41 +245,40 @@ func (trl *TagRoomList) RenderHeight() int {
return height return height
} }
func (trl *TagRoomList) DrawHeader(screen tcell.Screen) { func (trl *TagRoomList) DrawHeader(screen mauview.Screen) {
x, y, width, _ := trl.GetRect() width, _ := screen.Size()
roomCount := strconv.Itoa(trl.TotalLength()) roomCount := strconv.Itoa(trl.TotalLength())
// Draw tag name // Draw tag name
displayNameWidth := width - 1 - len(roomCount) 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 // Draw tag room count
roomCountX := x + len(trl.displayname) + 1 roomCountX := len(trl.displayname) + 1
roomCountWidth := width - 2 - len(trl.displayname) 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 { if len(trl.displayname) == 0 {
return return
} }
trl.DrawHeader(screen) trl.DrawHeader(screen)
x, y, width, height := trl.GetRect() width, height := screen.Size()
yLimit := y + height
items := trl.Visible() items := trl.Visible()
if trl.IsCollapsed() { if trl.IsCollapsed() {
screen.SetCell(x+width-1, y, tcell.StyleDefault, '▶') screen.SetCell(width-1, 0, tcell.StyleDefault, '▶')
return 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-- { for i := trl.Length() - 1; i >= 0; i-- {
if y+offsetY >= yLimit { if y >= height {
return return
} }
@ -288,18 +286,18 @@ func (trl *TagRoomList) Draw(screen tcell.Screen) {
lineWidth := width lineWidth := width
isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected
item.Draw(trl.parent, screen, x, y+offsetY, lineWidth, isSelected) item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected)
offsetY++ y++
} }
hasLess := trl.maxShown > 10 hasLess := trl.maxShown > 10
hasMore := trl.HasInvisibleRooms() hasMore := trl.HasInvisibleRooms()
if (hasLess || hasMore) && y+offsetY < yLimit { if (hasLess || hasMore) && y < height {
if hasMore { 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 { 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++
} }
} }

View File

@ -19,8 +19,8 @@ package ui
import ( import (
"os" "os"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
) )
@ -34,17 +34,18 @@ const (
) )
type GomuksUI struct { type GomuksUI struct {
gmx ifc.Gomuks gmx ifc.Gomuks
app *tview.Application app *mauview.Application
views *tview.Pages
mainView *MainView mainView *MainView
loginView *LoginView loginView *LoginView
views map[View]mauview.Component
} }
func init() { func init() {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault mauview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen mauview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 { if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 {
if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() { if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() {
os.Setenv("TCELLDB", "/usr/share/tcell/database") os.Setenv("TCELLDB", "/usr/share/tcell/database")
@ -54,20 +55,22 @@ func init() {
func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI { func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI {
ui := &GomuksUI{ ui := &GomuksUI{
gmx: gmx, gmx: gmx,
app: tview.NewApplication(), app: mauview.NewApplication(),
views: tview.NewPages(),
} }
ui.views.SetChangedFunc(ui.Render)
return ui return ui
} }
func (ui *GomuksUI) Init() { 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 { func (ui *GomuksUI) Start() error {
return ui.app.Run() return ui.app.Start()
} }
func (ui *GomuksUI) Stop() { func (ui *GomuksUI) Stop() {
@ -75,23 +78,21 @@ func (ui *GomuksUI) Stop() {
} }
func (ui *GomuksUI) Finish() { func (ui *GomuksUI) Finish() {
if ui.app.GetScreen() != nil { if ui.app.Screen() != nil {
ui.app.GetScreen().Fini() ui.app.Screen().Fini()
} }
} }
func (ui *GomuksUI) Render() { func (ui *GomuksUI) Render() {
ui.app.Draw() ui.app.Redraw()
} }
func (ui *GomuksUI) OnLogin() { func (ui *GomuksUI) OnLogin() {
ui.SetView(ViewMain) ui.SetView(ViewMain)
ui.app.SetFocus(ui.mainView)
} }
func (ui *GomuksUI) OnLogout() { func (ui *GomuksUI) OnLogout() {
ui.SetView(ViewLogin) ui.SetView(ViewLogin)
ui.app.SetFocus(ui.loginView)
} }
func (ui *GomuksUI) HandleNewPreferences() { func (ui *GomuksUI) HandleNewPreferences() {
@ -99,13 +100,11 @@ func (ui *GomuksUI) HandleNewPreferences() {
} }
func (ui *GomuksUI) SetView(name View) { func (ui *GomuksUI) SetView(name View) {
ui.views.SwitchToPage(string(name)) ui.app.Root = ui.views[name]
} focusable, ok := ui.app.Root.(mauview.Focusable)
if ok {
func (ui *GomuksUI) InitViews() tview.Primitive { focusable.Focus()
ui.views.AddPage(string(ViewLogin), ui.NewLoginView(), true, true) }
ui.views.AddPage(string(ViewMain), ui.NewMainView(), true, false)
return ui.views
} }
func (ui *GomuksUI) MainView() ifc.MainView { func (ui *GomuksUI) MainView() ifc.MainView {

View File

@ -17,66 +17,97 @@
package ui package ui
import ( import (
"maunium.net/go/tcell"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/tview" "maunium.net/go/mauview"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/widget"
) )
type LoginView struct { type LoginView struct {
*tview.Form *mauview.Form
homeserver *widget.AdvancedInputField container *mauview.Centerer
username *widget.AdvancedInputField
password *widget.AdvancedInputField homeserverLabel *mauview.TextField
error *widget.FormTextView 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 matrix ifc.MatrixContainer
config *config.Config config *config.Config
parent *GomuksUI parent *GomuksUI
} }
func (ui *GomuksUI) NewLoginView() tview.Primitive { func (ui *GomuksUI) NewLoginView() mauview.Component {
view := &LoginView{ view := &LoginView{
Form: tview.NewForm(), Form: mauview.NewForm(),
homeserver: widget.NewAdvancedInputField(), usernameLabel: mauview.NewTextField().SetText("Username"),
username: widget.NewAdvancedInputField(), passwordLabel: mauview.NewTextField().SetText("Password"),
password: widget.NewAdvancedInputField(), 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(), matrix: ui.gmx.Matrix(),
config: ui.gmx.Config(), config: ui.gmx.Config(),
parent: ui, parent: ui,
} }
hs := ui.gmx.Config().HS hs := ui.gmx.Config().HS
if len(hs) == 0 { view.homeserver.SetText(hs)
hs = "https://matrix.org" view.username.SetText(ui.gmx.Config().UserID)
} view.password.SetMaskCharacter('*')
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. view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorBlue)
AddFormItem(view.homeserver).AddFormItem(view.username).AddFormItem(view.password). view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorBlue)
AddButton("Log in", view.Login).
AddButton("Quit", ui.gmx.Stop).
SetButtonsAlign(tview.AlignCenter).
SetBorder(true).SetTitle("Log in to Matrix")
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 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) { 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 { if view.error == nil {
view.error = &widget.FormTextView{TextView: tview.NewTextView()} view.error = mauview.NewTextField().SetTextColor(tcell.ColorRed)
view.AddFormItem(view.error) view.AddComponent(view.error, 1, 9, 7, 1)
} }
view.error.SetText(err) view.error.SetText(err)
view.parent.Render()
} }
func (view *LoginView) Login() { func (view *LoginView) Login() {
@ -91,8 +122,8 @@ func (view *LoginView) Login() {
err = view.matrix.Login(mxid, password) err = view.matrix.Login(mxid, password)
if err != nil { if err != nil {
if httpErr, ok := err.(mautrix.HTTPError); ok { if httpErr, ok := err.(mautrix.HTTPError); ok {
if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok { if httpErr.RespError != nil {
view.Error(respErr.Err) view.Error(httpErr.RespError.Err)
} else { } else {
view.Error(httpErr.Message) view.Error(httpErr.Message)
} }

View File

@ -26,8 +26,8 @@ import (
"github.com/kyokomi/emoji" "github.com/kyokomi/emoji"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
@ -40,12 +40,16 @@ import (
) )
type MainView struct { type MainView struct {
*tview.Flex flex *mauview.Flex
roomList *RoomList roomList *RoomList
roomView *tview.Pages roomView *mauview.Box
currentRoom *RoomView
rooms map[string]*RoomView rooms map[string]*RoomView
cmdProcessor *CommandProcessor cmdProcessor *CommandProcessor
focused mauview.Focusable
modal mauview.Component
lastFocusTime time.Time lastFocusTime time.Time
@ -55,11 +59,10 @@ type MainView struct {
parent *GomuksUI parent *GomuksUI
} }
func (ui *GomuksUI) NewMainView() tview.Primitive { func (ui *GomuksUI) NewMainView() mauview.Component {
mainView := &MainView{ mainView := &MainView{
Flex: tview.NewFlex(), flex: mauview.NewFlex().SetDirection(mauview.FlexColumn),
roomList: NewRoomList(), roomView: mauview.NewBox(nil).SetBorder(false),
roomView: tview.NewPages(),
rooms: make(map[string]*RoomView), rooms: make(map[string]*RoomView),
matrix: ui.gmx.Matrix(), matrix: ui.gmx.Matrix(),
@ -67,13 +70,13 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
config: ui.gmx.Config(), config: ui.gmx.Config(),
parent: ui, parent: ui,
} }
mainView.roomList = NewRoomList(mainView)
mainView.cmdProcessor = NewCommandProcessor(mainView) mainView.cmdProcessor = NewCommandProcessor(mainView)
mainView. mainView.flex.
SetDirection(tview.FlexColumn). AddFixedComponent(mainView.roomList, 25).
AddItem(mainView.roomList, 25, 0, false). AddFixedComponent(widget.NewBorder(), 1).
AddItem(widget.NewBorder(), 1, 0, false). AddProportionalComponent(mainView.roomView, 1)
AddItem(mainView.roomView, 0, 1, true)
mainView.BumpFocus(nil) mainView.BumpFocus(nil)
ui.mainView = mainView ui.mainView = mainView
@ -81,18 +84,37 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
return mainView 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 { if view.config.Preferences.HideRoomList {
view.roomView.SetRect(view.GetRect())
view.roomView.Draw(screen) view.roomView.Draw(screen)
} else { } else {
view.Flex.Draw(screen) view.flex.Draw(screen)
}
if view.modal != nil {
view.modal.Draw(screen)
} }
} }
func (view *MainView) BumpFocus(roomView *RoomView) { func (view *MainView) BumpFocus(roomView *RoomView) {
view.lastFocusTime = time.Now() if roomView != nil {
view.MarkRead(roomView) view.lastFocusTime = time.Now()
view.MarkRead(roomView)
}
} }
func (view *MainView) MarkRead(roomView *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) { 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() { view.parent.app.Suspend(func() {
print("\033[2J\033[0;0H") print("\033[2J\033[0;0H")
// We don't know how much space there exactly is. Too few messages looks weird, // 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 { func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool {
view.BumpFocus(roomView) view.BumpFocus(view.currentRoom)
k := key.Key() if view.modal != nil {
c := key.Rune() return view.modal.OnKeyEvent(event)
if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt { }
k := event.Key()
c := event.Rune()
if event.Modifiers() == tcell.ModCtrl || event.Modifiers() == tcell.ModAlt {
switch { switch {
case k == tcell.KeyDown: case k == tcell.KeyDown:
view.SwitchRoom(view.roomList.Next()) view.SwitchRoom(view.roomList.Next())
case k == tcell.KeyUp: case k == tcell.KeyUp:
view.SwitchRoom(view.roomList.Previous()) view.SwitchRoom(view.roomList.Previous())
case k == tcell.KeyEnter: case k == tcell.KeyEnter:
searchModal := NewFuzzySearchModal(view, 42, 12) view.ShowModal(NewFuzzySearchModal(view, 42, 12))
view.parent.views.AddPage("fuzzy-search-modal", searchModal, true, true) case k == tcell.KeyHome:
view.parent.app.SetFocus(searchModal) 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': case c == 'a':
view.SwitchRoom(view.roomList.NextWithActivity()) view.SwitchRoom(view.roomList.NextWithActivity())
case c == 'l': case c == 'l':
view.ShowBare(roomView) view.ShowBare(view.currentRoom)
default: default:
return key goto defaultHandler
} }
return true
} else if k == tcell.KeyAltDown || k == tcell.KeyCtrlDown { } else if k == tcell.KeyAltDown || k == tcell.KeyCtrlDown {
view.SwitchRoom(view.roomList.Next()) view.SwitchRoom(view.roomList.Next())
return true
} else if k == tcell.KeyAltUp || k == tcell.KeyCtrlUp { } else if k == tcell.KeyAltUp || k == tcell.KeyCtrlUp {
view.SwitchRoom(view.roomList.Previous()) 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 { return true
msgView := roomView.MessageView() } 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) { if msgView.IsAtTop() && (k == tcell.KeyPgUp || k == tcell.KeyUp) {
go view.LoadHistory(roomView.Room.ID) go view.LoadHistory(view.currentRoom.Room.ID)
} }
switch k { switch k {
@ -219,80 +261,72 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *
msgView.AddScrollOffset(msgView.Height() / 2) msgView.AddScrollOffset(msgView.Height() / 2)
case tcell.KeyPgDn: case tcell.KeyPgDn:
msgView.AddScrollOffset(-msgView.Height() / 2) msgView.AddScrollOffset(-msgView.Height() / 2)
case tcell.KeyUp: default:
msgView.AddScrollOffset(1) goto defaultHandler
case tcell.KeyDown:
msgView.AddScrollOffset(-1)
case tcell.KeyHome:
msgView.AddScrollOffset(msgView.TotalHeight())
case tcell.KeyEnd:
msgView.AddScrollOffset(-msgView.TotalHeight())
} }
} else { return true
return key } 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 const WheelScrollOffsetDiff = 3
func isInArea(x, y int, p tview.Primitive) bool { func (view *MainView) OnMouseEvent(event mauview.MouseEvent) bool {
rx, ry, rw, rh := p.GetRect() if view.config.Preferences.HideRoomList {
return x >= rx && y >= ry && x < rx+rw && y < ry+rh return view.roomView.OnMouseEvent(event)
} }
return view.flex.OnMouseEvent(event)
func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse { /*if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
if event.Buttons() == tcell.ButtonNone || event.HasMotion() { return false
return event
} }
view.BumpFocus(roomView)
msgView := roomView.MessageView() view.BumpFocus(view.currentRoom)
x, y := event.Position() x, y := event.Position()
switch { switch {
case isInArea(x, y, msgView): case x >= 27:
mx, my, _, _ := msgView.GetRect() view.roomView.OnMouseEvent(mauview.OffsetMouseEvent(event, -27, 0))
switch event.Buttons() { view.roomView.Focus()
case tcell.WheelUp: view.focused = view.roomView
if msgView.IsAtTop() { case x <= 25:
go view.LoadHistory(roomView.Room.ID) view.roomList.OnMouseEvent(event)
} else { view.roomList.Focus()
msgView.AddScrollOffset(WheelScrollOffsetDiff) view.focused = view.roomList
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()
}
}
default: default:
debug.Print("Unhandled mouse event:", event.Buttons(), event.Modifiers(), x, y) 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) { func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
@ -300,29 +334,19 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
return return
} }
view.roomView.SwitchToPage(room.ID)
roomView := view.rooms[room.ID] roomView := view.rooms[room.ID]
if roomView == nil { if roomView == nil {
debug.Print("Tried to switch to non-nil room with nil roomView!") debug.Print("Tried to switch to non-nil room with nil roomView!")
debug.Print(tag, room) debug.Print(tag, room)
return return
} }
view.roomView.SetInnerComponent(roomView)
view.currentRoom = roomView
view.MarkRead(roomView) view.MarkRead(roomView)
view.roomList.SetSelected(tag, room) view.roomList.SetSelected(tag, room)
view.parent.app.SetFocus(view)
view.parent.Render() 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() { func (view *MainView) SaveAllHistory() {
for _, room := range view.rooms { for _, room := range view.rooms {
err := room.SaveHistory(view.config.HistoryDir) err := room.SaveHistory(view.config.HistoryDir)
@ -333,14 +357,11 @@ func (view *MainView) SaveAllHistory() {
} }
func (view *MainView) addRoomPage(room *rooms.Room) { func (view *MainView) addRoomPage(room *rooms.Room) {
if !view.roomView.HasPage(room.ID) { if _, ok := view.rooms[room.ID]; !ok {
roomView := NewRoomView(view, room). roomView := NewRoomView(view, room).
SetInputSubmitFunc(view.InputSubmit). SetInputSubmitFunc(view.InputSubmit).
SetInputChangedFunc(view.InputChanged). SetInputChangedFunc(view.InputChanged)
SetInputCapture(view.KeyEventHandler).
SetMouseCapture(view.MouseEventHandler)
view.rooms[room.ID] = roomView view.rooms[room.ID] = roomView
view.roomView.AddPage(room.ID, roomView, true, false)
roomView.UpdateUserList() roomView.UpdateUserList()
_, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir) _, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir)
@ -387,7 +408,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
view.roomList.Remove(room) view.roomList.Remove(room)
view.SwitchRoom(view.roomList.Selected()) view.SwitchRoom(view.roomList.Selected())
view.roomView.RemovePage(room.ID)
delete(view.rooms, room.ID) delete(view.rooms, room.ID)
view.parent.Render() view.parent.Render()
@ -395,7 +415,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
func (view *MainView) SetRooms(rooms map[string]*rooms.Room) { func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
view.roomList.Clear() view.roomList.Clear()
view.roomView.Clear()
view.rooms = make(map[string]*RoomView) view.rooms = make(map[string]*RoomView)
for _, room := range rooms { for _, room := range rooms {
if room.HasLeft { if room.HasLeft {

View File

@ -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 <https://www.gnu.org/licenses/>.
// 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)
}
}
}

View File

@ -17,8 +17,8 @@
package widget package widget
import ( import (
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/tview"
) )
// Border is a simple tview widget that renders a horizontal or vertical bar. // 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 height is 1, the bar will be horizontal.
// If the width nor the height are 1, nothing will be rendered. // If the width nor the height are 1, nothing will be rendered.
type Border struct { type Border struct {
*tview.Box Style tcell.Style
} }
// NewBorder wraps a new tview Box into a new Border. // NewBorder wraps a new tview Box into a new Border.
func NewBorder() *Border { func NewBorder() *Border {
return &Border{tview.NewBox()} return &Border{
Style: tcell.StyleDefault.Foreground(mauview.Styles.BorderColor),
}
} }
func (border *Border) Draw(screen tcell.Screen) { func (border *Border) Draw(screen mauview.Screen) {
background := tcell.StyleDefault.Background(border.GetBackgroundColor()).Foreground(border.GetBorderColor()) width, height := screen.Size()
x, y, width, height := border.GetRect()
if width == 1 { if width == 1 {
for borderY := y; borderY < y+height; borderY++ { for borderY := 0; borderY < height; borderY++ {
screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background) screen.SetContent(0, borderY, mauview.Borders.Vertical, nil, border.Style)
} }
} else if height == 1 { } else if height == 1 {
for borderX := x; borderX < x+width; borderX++ { for borderX := 0; borderX < width; borderX++ {
screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background) 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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -18,28 +18,31 @@ package widget
import ( import (
"fmt" "fmt"
"github.com/mattn/go-runewidth"
"maunium.net/go/tcell"
"maunium.net/go/tview"
"strconv" "strconv"
"github.com/mattn/go-runewidth"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
) )
func WriteLineSimple(screen tcell.Screen, line string, x, y int) { func WriteLineSimple(screen mauview.Screen, line string, x, y int) {
WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault) WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
} }
func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) { func WriteLineSimpleColor(screen mauview.Screen, line string, x, y int, color tcell.Color) {
WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(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)) 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 offsetX := 0
if align == tview.AlignRight { if align == mauview.AlignRight {
offsetX = maxWidth - runewidth.StringWidth(line) // TODO is mauview.StringWidth correct here?
offsetX = maxWidth - mauview.StringWidth(line)
} }
if offsetX < 0 { if offsetX < 0 {
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) padding := strconv.Itoa(maxWidth)
if align == tview.AlignRight { if align == mauview.AlignRight {
line = fmt.Sprintf("%"+padding+"s", line) line = fmt.Sprintf("%"+padding+"s", line)
} else { } else {
line = fmt.Sprintf("%-"+padding+"s", line) line = fmt.Sprintf("%-"+padding+"s", line)
} }
WriteLine(screen, tview.AlignLeft, line, x, y, maxWidth, style) WriteLine(screen, mauview.AlignLeft, line, x, y, maxWidth, style)
} }