about summary refs log tree commit diff stats
path: root/widgets
diff options
context:
space:
mode:
authorJeffas <dev@jeffas.io>2019-09-05 23:32:36 +0100
committerDrew DeVault <sir@cmpwn.com>2019-09-11 11:41:34 -0400
commitf6216bb6213a15cdcdf50089f3c4a8e9a30d9337 (patch)
tree3075b68379ae7c13cb9eedc53b7cf033672478a9 /widgets
parenta441e3b3a55d11e7cc9444298051ff339d1b7519 (diff)
downloadaerc-f6216bb6213a15cdcdf50089f3c4a8e9a30d9337.tar.gz
Add Mouseable
This adds the Mouseable interface. When this is implemented for a
component that item can accept and process mouseevents.

At the top level when a mouse event is received it is passed to the
grid's handler and then it trickles down until it reaches a component
that can actually handle it, such as the tablist, dirlist or msglist.

A mouse event is passed so that components can handle other things such
as scrolling with the mousewheel. The components themselves then perform
the necessary actions.

Clicking emails in the messagelist opens them in a new tab.

Textinputs can be clicked to position the cursor inside them.

Mouseevents are not forwarded to the terminal at the moment.

Elements which do not handle mouse events are not required to implement
the Mouseable interface.
Diffstat (limited to 'widgets')
-rw-r--r--widgets/account-wizard.go2
-rw-r--r--widgets/account.go11
-rw-r--r--widgets/aerc.go41
-rw-r--r--widgets/compose.go21
-rw-r--r--widgets/dirlist.go29
-rw-r--r--widgets/msglist.go45
-rw-r--r--widgets/msgviewer.go72
-rw-r--r--widgets/terminal.go14
8 files changed, 212 insertions, 23 deletions
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 5acd26c..904013f 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -523,7 +523,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
 	}
 	wizard.conf.Accounts = append(wizard.conf.Accounts, account)
 
-	view := NewAccountView(wizard.conf, &account,
+	view := NewAccountView(wizard.aerc, wizard.conf, &account,
 		wizard.aerc.logger, wizard.aerc)
 	wizard.aerc.accounts[account.Name] = view
 	wizard.aerc.NewTab(view, account.Name)
diff --git a/widgets/account.go b/widgets/account.go
index 688b660..1220753 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -17,6 +17,7 @@ import (
 
 type AccountView struct {
 	acct    *config.AccountConfig
+	aerc    *Aerc
 	conf    *config.AercConfig
 	dirlist *DirectoryList
 	grid    *ui.Grid
@@ -26,7 +27,7 @@ type AccountView struct {
 	worker  *types.Worker
 }
 
-func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
+func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig,
 	logger *log.Logger, host TabHost) *AccountView {
 
 	grid := ui.NewGrid().Rows([]ui.GridSpec{
@@ -42,6 +43,7 @@ func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
 			Color(tcell.ColorDefault, tcell.ColorRed)
 		return &AccountView{
 			acct:   acct,
+			aerc:   aerc,
 			grid:   grid,
 			host:   host,
 			logger: logger,
@@ -53,11 +55,12 @@ func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
 		grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT))
 	}
 
-	msglist := NewMessageList(conf, logger)
+	msglist := NewMessageList(conf, logger, aerc)
 	grid.AddChild(msglist).At(0, 1)
 
 	view := &AccountView{
 		acct:    acct,
+		aerc:    aerc,
 		conf:    conf,
 		dirlist: dirlist,
 		grid:    grid,
@@ -124,6 +127,10 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
 	acct.grid.Draw(ctx)
 }
 
+func (acct *AccountView) MouseEvent(localX int, localY int, event tcell.Event) {
+	acct.grid.MouseEvent(localX, localY, event)
+}
+
 func (acct *AccountView) Focus(focus bool) {
 	// TODO: Unfocus children I guess
 }
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 87009cd..fe3c1e2 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -74,7 +74,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
 	conf.Triggers.ExecuteCommand = cmd
 
 	for i, acct := range conf.Accounts {
-		view := NewAccountView(conf, &conf.Accounts[i], logger, aerc)
+		view := NewAccountView(aerc, conf, &conf.Accounts[i], logger, aerc)
 		aerc.accounts[acct.Name] = view
 		tabs.Add(view, acct.Name)
 	}
@@ -85,6 +85,22 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
 		aerc.NewTab(wizard, "New account")
 	}
 
+	tabs.CloseTab = func(index int) {
+		switch content := aerc.tabs.Tabs[index].Content.(type) {
+		case *AccountView:
+			return
+		case *AccountWizard:
+			return
+		case *Composer:
+			aerc.RemoveTab(content)
+			content.Close()
+		case *Terminal:
+			content.Close(nil)
+		case *MessageViewer:
+			aerc.RemoveTab(content)
+		}
+	}
+
 	return aerc
 }
 
@@ -235,7 +251,12 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
 			return false
 		}
 	case *tcell.EventMouse:
-		aerc.tabs.MouseEvent(event)
+		if event.Buttons() == tcell.ButtonNone {
+			return false
+		}
+		x, y := event.Position()
+		aerc.grid.MouseEvent(x, y, event)
+		return true
 	}
 	return false
 }
@@ -260,8 +281,8 @@ func (aerc *Aerc) SelectedTab() ui.Drawable {
 	return aerc.tabs.Tabs[aerc.tabs.Selected].Content
 }
 
-func (aerc *Aerc) NewTab(drawable ui.Drawable, name string) *ui.Tab {
-	tab := aerc.tabs.Add(drawable, name)
+func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab {
+	tab := aerc.tabs.Add(clickable, name)
 	aerc.tabs.Select(len(aerc.tabs.Tabs) - 1)
 	return tab
 }
@@ -275,19 +296,11 @@ func (aerc *Aerc) ReplaceTab(tabSrc ui.Drawable, tabTarget ui.Drawable, name str
 }
 
 func (aerc *Aerc) NextTab() {
-	next := aerc.tabs.Selected + 1
-	if next >= len(aerc.tabs.Tabs) {
-		next = 0
-	}
-	aerc.tabs.Select(next)
+	aerc.tabs.NextTab()
 }
 
 func (aerc *Aerc) PrevTab() {
-	next := aerc.tabs.Selected - 1
-	if next < 0 {
-		next = len(aerc.tabs.Tabs) - 1
-	}
-	aerc.tabs.Select(next)
+	aerc.tabs.PrevTab()
 }
 
 func (aerc *Aerc) SelectTab(name string) bool {
diff --git a/widgets/compose.go b/widgets/compose.go
index bd4301a..0e7f09e 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -40,10 +40,12 @@ type Composer struct {
 	worker      *types.Worker
 
 	layout    HeaderLayout
-	focusable []ui.DrawableInteractive
+	focusable []ui.MouseableDrawableInteractive
 	focused   int
 
 	onClose []func(ti *Composer)
+
+	width int
 }
 
 func NewComposer(conf *config.AercConfig,
@@ -87,10 +89,10 @@ func NewComposer(conf *config.AercConfig,
 func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (
 	newLayout HeaderLayout,
 	editors map[string]*headerEditor,
-	focusable []ui.DrawableInteractive,
+	focusable []ui.MouseableDrawableInteractive,
 ) {
 	editors = make(map[string]*headerEditor)
-	focusable = make([]ui.DrawableInteractive, 0)
+	focusable = make([]ui.MouseableDrawableInteractive, 0)
 
 	for _, row := range layout {
 		for _, h := range row {
@@ -99,7 +101,7 @@ func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (
 			switch h {
 			case "From":
 				// Prepend From to support backtab
-				focusable = append([]ui.DrawableInteractive{e}, focusable...)
+				focusable = append([]ui.MouseableDrawableInteractive{e}, focusable...)
 			default:
 				focusable = append(focusable, e)
 			}
@@ -176,6 +178,7 @@ func (c *Composer) OnClose(fn func(composer *Composer)) {
 }
 
 func (c *Composer) Draw(ctx *ui.Context) {
+	c.width = ctx.Width()
 	c.grid.Draw(ctx)
 }
 
@@ -617,6 +620,16 @@ func (he *headerEditor) Draw(ctx *ui.Context) {
 	he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
 }
 
+func (he *headerEditor) MouseEvent(localX int, localY int, event tcell.Event) {
+	switch event := event.(type) {
+	case *tcell.EventMouse:
+		width := runewidth.StringWidth(he.name + " ")
+		if localX >= width {
+			he.input.MouseEvent(localX-width, localY, event)
+		}
+	}
+}
+
 func (he *headerEditor) Invalidate() {
 	he.input.Invalidate()
 }
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 33119dd..ec73082 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -137,6 +137,35 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
 	}
 }
 
+func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) {
+	switch event := event.(type) {
+	case *tcell.EventMouse:
+		switch event.Buttons() {
+		case tcell.Button1:
+			clickedDir, ok := dirlist.Clicked(localX, localY)
+			if ok {
+				dirlist.Select(clickedDir)
+			}
+		case tcell.WheelDown:
+			dirlist.Next()
+		case tcell.WheelUp:
+			dirlist.Prev()
+		}
+	}
+}
+
+func (dirlist *DirectoryList) Clicked(x int, y int) (string, bool) {
+	if dirlist.dirs == nil || len(dirlist.dirs) == 0 {
+		return "", false
+	}
+	for i, name := range dirlist.dirs {
+		if i == y {
+			return name, true
+		}
+	}
+	return "", false
+}
+
 func (dirlist *DirectoryList) NextPrev(delta int) {
 	curIdx := sort.SearchStrings(dirlist.dirs, dirlist.selected)
 	if curIdx == len(dirlist.dirs) {
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 8ed716b..b7c921c 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -25,6 +25,7 @@ type MessageList struct {
 	spinner       *Spinner
 	store         *lib.MessageStore
 	isInitalizing bool
+	aerc          *Aerc
 }
 
 type msgSorter struct {
@@ -55,12 +56,13 @@ func (s *msgSorter) Swap(i, j int) {
 	s.uids[j] = tmp
 }
 
-func NewMessageList(conf *config.AercConfig, logger *log.Logger) *MessageList {
+func NewMessageList(conf *config.AercConfig, logger *log.Logger, aerc *Aerc) *MessageList {
 	ml := &MessageList{
 		conf:          conf,
 		logger:        logger,
 		spinner:       NewSpinner(&conf.Ui),
 		isInitalizing: true,
+		aerc:          aerc,
 	}
 	ml.spinner.OnInvalidate(func(_ ui.Drawable) {
 		ml.Invalidate()
@@ -161,6 +163,47 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 	}
 }
 
+func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
+	switch event := event.(type) {
+	case *tcell.EventMouse:
+		switch event.Buttons() {
+		case tcell.Button1:
+			if ml.aerc == nil {
+				return
+			}
+			selectedMsg, ok := ml.Clicked(localX, localY)
+			if ok {
+				ml.Select(selectedMsg)
+				acct := ml.aerc.SelectedAccount()
+				if acct.Messages().Empty() {
+					return
+				}
+				store := acct.Messages().Store()
+				msg := acct.Messages().Selected()
+				if msg == nil {
+					return
+				}
+				viewer := NewMessageViewer(acct, ml.aerc.Config(), store, msg)
+				ml.aerc.NewTab(viewer, msg.Envelope.Subject)
+			}
+		case tcell.WheelDown:
+			ml.store.Next()
+			ml.Scroll()
+		case tcell.WheelUp:
+			ml.store.Prev()
+			ml.Scroll()
+		}
+	}
+}
+
+func (ml *MessageList) Clicked(x, y int) (int, bool) {
+	store := ml.Store()
+	if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
+		return 0, false
+	}
+	return y + ml.scroll, true
+}
+
 func (ml *MessageList) Height() int {
 	return ml.height
 }
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index e210616..c179070 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -42,6 +42,9 @@ type PartSwitcher struct {
 	selected       int
 	showHeaders    bool
 	alwaysShowMime bool
+
+	height int
+	mv     *MessageViewer
 }
 
 func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
@@ -77,7 +80,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
 	grid.AddChild(header).At(0, 0)
 	grid.AddChild(switcher).At(1, 0)
 
-	return &MessageViewer{
+	mv := &MessageViewer{
 		acct:     acct,
 		conf:     conf,
 		grid:     grid,
@@ -85,6 +88,9 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
 		store:    store,
 		switcher: switcher,
 	}
+	switcher.mv = mv
+
+	return mv
 }
 
 func fmtHeader(msg *models.MessageInfo, header string) string {
@@ -194,6 +200,13 @@ func (mv *MessageViewer) Draw(ctx *ui.Context) {
 	mv.grid.Draw(ctx)
 }
 
+func (mv *MessageViewer) MouseEvent(localX int, localY int, event tcell.Event) {
+	if mv.err != nil {
+		return
+	}
+	mv.grid.MouseEvent(localX, localY, event)
+}
+
 func (mv *MessageViewer) Invalidate() {
 	mv.grid.Invalidate()
 }
@@ -295,6 +308,7 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
 		return
 	}
 	// TODO: cap height and add scrolling for messages with many parts
+	ps.height = ctx.Height()
 	y := ctx.Height() - height
 	for i, part := range ps.parts {
 		style := tcell.StyleDefault.Reverse(ps.selected == i)
@@ -311,6 +325,62 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
 		0, 0, ctx.Width(), ctx.Height()-height))
 }
 
+func (ps *PartSwitcher) MouseEvent(localX int, localY int, event tcell.Event) {
+	switch event := event.(type) {
+	case *tcell.EventMouse:
+		switch event.Buttons() {
+		case tcell.Button1:
+			height := len(ps.parts)
+			y := ps.height - height
+			if localY < y {
+				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
+			}
+			for i, _ := range ps.parts {
+				if localY != y+i {
+					continue
+				}
+				if ps.parts[i].part.MIMEType == "multipart" {
+					continue
+				}
+				if ps.parts[ps.selected].term != nil {
+					ps.parts[ps.selected].term.Focus(false)
+				}
+				ps.selected = i
+				ps.Invalidate()
+				if ps.parts[ps.selected].term != nil {
+					ps.parts[ps.selected].term.Focus(true)
+				}
+			}
+		case tcell.WheelDown:
+			height := len(ps.parts)
+			y := ps.height - height
+			if localY < y {
+				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
+			}
+			if ps.parts[ps.selected].term != nil {
+				ps.parts[ps.selected].term.Focus(false)
+			}
+			ps.mv.NextPart()
+			if ps.parts[ps.selected].term != nil {
+				ps.parts[ps.selected].term.Focus(true)
+			}
+		case tcell.WheelUp:
+			height := len(ps.parts)
+			y := ps.height - height
+			if localY < y {
+				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
+			}
+			if ps.parts[ps.selected].term != nil {
+				ps.parts[ps.selected].term.Focus(false)
+			}
+			ps.mv.PreviousPart()
+			if ps.parts[ps.selected].term != nil {
+				ps.parts[ps.selected].term.Focus(true)
+			}
+		}
+	}
+}
+
 func (mv *MessageViewer) Event(event tcell.Event) bool {
 	return mv.switcher.Event(event)
 }
diff --git a/widgets/terminal.go b/widgets/terminal.go
index 008a36f..6ad6904 100644
--- a/widgets/terminal.go
+++ b/widgets/terminal.go
@@ -311,6 +311,20 @@ func (term *Terminal) Draw(ctx *ui.Context) {
 	}
 }
 
+func (term *Terminal) MouseEvent(localX int, localY int, event tcell.Event) {
+	switch event := event.(type) {
+	case *tcell.EventMouse:
+		if term.OnEvent != nil {
+			if term.OnEvent(event) {
+				return
+			}
+		}
+		if term.closed {
+			return
+		}
+	}
+}
+
 func (term *Terminal) Focus(focus bool) {
 	if term.closed {
 		return