Преглед изворни кода

feat(ui): add backdrop for dialogs (#736)

Ayyan пре 2 месеци
родитељ
комит
f0a2e8d5da

+ 2 - 2
go.mod

@@ -7,7 +7,7 @@ go 1.25.3
 require (
 	github.com/BurntSushi/toml v1.6.0
 	github.com/andybalholm/brotli v1.2.0
-	github.com/ayn2op/tview v0.0.0-20260131155949-225a892e84f6
+	github.com/ayn2op/tview v0.0.0-20260205013318-8c0da03daa84
 	github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
 	github.com/diamondburned/arikawa/v3 v3.6.1-0.20250928004212-a891a653eb26
 	github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d
@@ -51,7 +51,7 @@ require (
 	go4.org v0.0.0-20260112195520-a5071408f32f // indirect
 	golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96 // indirect
 	golang.org/x/image v0.35.0 // indirect
-	golang.org/x/mobile v0.0.0-20260120165949-40bd9ace6ce4 // indirect
+	golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 // indirect
 	golang.org/x/sys v0.40.0 // indirect
 	golang.org/x/term v0.39.0 // indirect
 	golang.org/x/text v0.33.0 // indirect

+ 4 - 4
go.sum

@@ -8,8 +8,8 @@ github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
 github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
 github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
-github.com/ayn2op/tview v0.0.0-20260131155949-225a892e84f6 h1:3j6AxMluaTkZKkLmPiuzY1nranGEEJOs3wVbJQjReuQ=
-github.com/ayn2op/tview v0.0.0-20260131155949-225a892e84f6/go.mod h1:g6IOdF9SlnVZMDnRABANP8I0LFseKyYxWqEkzBSL5ho=
+github.com/ayn2op/tview v0.0.0-20260205013318-8c0da03daa84 h1:SGBLuL9Ks85RRmLuY9SSJwW4//q1yR5OnO1r//2gMa4=
+github.com/ayn2op/tview v0.0.0-20260205013318-8c0da03daa84/go.mod h1:g6IOdF9SlnVZMDnRABANP8I0LFseKyYxWqEkzBSL5ho=
 github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
 github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -111,8 +111,8 @@ golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96 h1:wJ3cDLvYRAWzRt6f3e2
 golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96/go.mod h1:hq/Ge0xSczE7aHicXVhn3Kd0j3hOtWQR4KEgAwemgdk=
 golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
 golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
-golang.org/x/mobile v0.0.0-20260120165949-40bd9ace6ce4 h1:C3JuLOLhdaE75vk5m7u18NvZciRk+lnO34xcXl3NPTU=
-golang.org/x/mobile v0.0.0-20260120165949-40bd9ace6ce4/go.mod h1:yHJY0EGzMJ0i5ONrrhdpDSSnoyres5LO7D2hSIbJJ5I=
+golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 h1:NiJtT7g4ncNFVjVZMAYNBrPSNhIjFYPj8UKA8MEw2A4=
+golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3/go.mod h1:wReH3Q1agKmmLapipWFnd4NSs8KPz3fK6mSEZjXLkrg=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

+ 5 - 0
internal/config/config.toml

@@ -190,3 +190,8 @@ min_width = 20
 # Maximum height
 # 0 = make the list as tall as needed
 max_height = 0
+
+[theme.dialog]
+style = {}
+# Background style: everything else behind the dialog
+background_style = { attributes = "dim" }

+ 6 - 0
internal/config/theme.go

@@ -156,6 +156,11 @@ type (
 		MaxHeight uint `toml:"max_height"`
 	}
 
+	DialogTheme struct {
+		Style           StyleWrapper `toml:"foreground_style"`
+		BackgroundStyle StyleWrapper `toml:"background_style"`
+	}
+
 	Theme struct {
 		Title        TitleTheme        `toml:"title"`
 		Footer       FooterTheme       `toml:"footer"`
@@ -163,6 +168,7 @@ type (
 		GuildsTree   GuildsTreeTheme   `toml:"guilds_tree"`
 		MessagesList MessagesListTheme `toml:"messages_list"`
 		MentionsList MentionsListTheme `toml:"mentions_list"`
+		Dialog       DialogTheme       `toml:"dialog"`
 	}
 )
 

+ 8 - 4
internal/ui/chat/message_input.go

@@ -3,6 +3,7 @@ package chat
 import (
 	"bytes"
 	"fmt"
+	"github.com/ayn2op/tview/layers"
 	"io"
 	"log/slog"
 	"os"
@@ -532,8 +533,12 @@ func (mi *messageInput) showMentionList() {
 	l.SetRect(x, y, w, h)
 
 	mi.chatView.
-		AddAndSwitchToPage(mentionsListPageName, l, false).
-		ShowPage(flexPageName)
+		AddLayer(l,
+			layers.WithName(mentionsListPageName),
+			layers.WithResize(false),
+			layers.WithVisible(true),
+		).
+		SendToFront(mentionsListPageName)
 	mi.chatView.app.SetFocus(mi)
 }
 
@@ -586,8 +591,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
 // used by chatView
 func (mi *messageInput) removeMentionsList() {
 	mi.chatView.
-		RemovePage(mentionsListPageName).
-		SwitchToPage(flexPageName)
+		RemoveLayer(mentionsListPageName)
 }
 
 func (mi *messageInput) stopTabCompletion() {

+ 10 - 3
internal/ui/chat/messages_list.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"github.com/ayn2op/tview/layers"
 	"io"
 	"log/slog"
 	"net/http"
@@ -509,7 +510,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
 		SetHighlightFullLine(true).
 		ShowSecondaryText(false).
 		SetDoneFunc(func() {
-			ml.chatView.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
+			ml.chatView.RemoveLayer(attachmentsListPageName)
 			ml.chatView.app.SetFocus(ml)
 		})
 	list.
@@ -546,8 +547,14 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
 	}
 
 	ml.chatView.
-		AddAndSwitchToPage(attachmentsListPageName, ui.Centered(list, 0, 0), true).
-		ShowPage(flexPageName)
+		AddLayer(
+			ui.Centered(list, 0, 0),
+			layers.WithName(attachmentsListPageName),
+			layers.WithResize(true),
+			layers.WithVisible(true),
+			layers.WithOverlay(),
+		).
+		SendToFront(attachmentsListPageName)
 }
 
 func (ml *messagesList) openAttachment(attachment discord.Attachment) {

+ 24 - 10
internal/ui/chat/view.go

@@ -2,6 +2,7 @@ package chat
 
 import (
 	"fmt"
+	"github.com/ayn2op/tview/layers"
 	"log/slog"
 	"sync"
 	"time"
@@ -27,7 +28,7 @@ const (
 )
 
 type View struct {
-	*tview.Pages
+	*layers.Layers
 
 	mainFlex  *tview.Flex
 	rightFlex *tview.Flex
@@ -52,7 +53,7 @@ type View struct {
 
 func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View {
 	v := &View{
-		Pages: tview.NewPages(),
+		Layers: layers.New(),
 
 		mainFlex:  tview.NewFlex(),
 		rightFlex: tview.NewFlex(),
@@ -63,12 +64,14 @@ func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View
 		cfg:      cfg,
 		onLogout: onLogout,
 	}
+
 	v.guildsTree = newGuildsTree(cfg, v)
 	v.messagesList = newMessagesList(cfg, v)
 	v.messageInput = newMessageInput(cfg, v)
 	v.channelsPicker = newChannelsPicker(cfg, v)
 	v.channelsPicker.SetCancelFunc(v.closePicker)
 
+	v.SetBackgroundLayerStyle(v.cfg.Theme.Dialog.BackgroundStyle.Style)
 	v.SetInputCapture(v.onInputCapture)
 	v.buildLayout()
 	return v
@@ -100,11 +103,11 @@ func (v *View) buildLayout() {
 		AddItem(v.guildsTree, 0, 1, true).
 		AddItem(v.rightFlex, 0, 4, false)
 
-	v.AddAndSwitchToPage(flexPageName, v.mainFlex, true)
+	v.AddLayer(v.mainFlex, layers.WithName(flexPageName), layers.WithResize(true), layers.WithVisible(true))
 }
 
 func (v *View) togglePicker() {
-	if v.HasPage(channelsPickerPageName) {
+	if v.HasLayer(channelsPickerPageName) {
 		v.closePicker()
 	} else {
 		v.openPicker()
@@ -112,12 +115,18 @@ func (v *View) togglePicker() {
 }
 
 func (v *View) openPicker() {
-	v.AddAndSwitchToPage(channelsPickerPageName, ui.Centered(v.channelsPicker, v.cfg.Picker.Width, v.cfg.Picker.Height), true).ShowPage(flexPageName)
+	v.AddLayer(
+		ui.Centered(v.channelsPicker, v.cfg.Picker.Width, v.cfg.Picker.Height),
+		layers.WithName(channelsPickerPageName),
+		layers.WithResize(true),
+		layers.WithVisible(true),
+		layers.WithOverlay(),
+	).SendToFront(channelsPickerPageName)
 	v.channelsPicker.update()
 }
 
 func (v *View) closePicker() {
-	v.RemovePage(channelsPickerPageName).SwitchToPage(flexPageName)
+	v.RemoveLayer(channelsPickerPageName)
 	v.channelsPicker.Update()
 }
 
@@ -235,17 +244,22 @@ func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(lab
 		SetText(prompt).
 		AddButtons(buttons).
 		SetDoneFunc(func(_ int, buttonLabel string) {
-			v.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
+			v.RemoveLayer(confirmModalPageName)
 			v.app.SetFocus(previousFocus)
 
 			if onDone != nil {
 				onDone(buttonLabel)
 			}
 		})
-
 	v.
-		AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
-		ShowPage(flexPageName)
+		AddLayer(
+			ui.Centered(modal, 0, 0),
+			layers.WithName(confirmModalPageName),
+			layers.WithResize(true),
+			layers.WithVisible(true),
+			layers.WithOverlay(),
+		).
+		SendToFront(confirmModalPageName)
 }
 
 func (v *View) onReadUpdate(event *read.UpdateEvent) {

+ 38 - 15
internal/ui/login/form.go

@@ -4,6 +4,9 @@ import (
 	"errors"
 	"log/slog"
 
+	"github.com/ayn2op/tview/layers"
+	"github.com/gdamore/tcell/v3"
+
 	"github.com/ayn2op/discordo/internal/config"
 	"github.com/ayn2op/discordo/internal/keyring"
 	"github.com/ayn2op/discordo/internal/ui"
@@ -20,7 +23,7 @@ const (
 type DoneFn = func(token string)
 
 type Form struct {
-	*tview.Pages
+	*layers.Layers
 	app  *tview.Application
 	cfg  *config.Config
 	form *tview.Form
@@ -29,18 +32,19 @@ type Form struct {
 
 func NewForm(app *tview.Application, cfg *config.Config, done DoneFn) *Form {
 	f := &Form{
-		Pages: tview.NewPages(),
-		app:   app,
-		cfg:   cfg,
-		form:  tview.NewForm(),
-		done:  done,
+		Layers: layers.New(),
+		app:    app,
+		cfg:    cfg,
+		form:   tview.NewForm(),
+		done:   done,
 	}
 
 	f.form.
 		AddPasswordField("Token", "", 0, 0, nil).
 		AddButton("Login", f.login).
 		AddButton("Login with QR", f.loginWithQR)
-	f.AddAndSwitchToPage(formPageName, f.form, true)
+	f.SetBackgroundLayerStyle(f.cfg.Theme.Dialog.BackgroundStyle.Style)
+	f.AddLayer(f.form, layers.WithName(formPageName), layers.WithResize(true), layers.WithVisible(true))
 	return f
 }
 
@@ -69,14 +73,33 @@ func (f *Form) onError(err error) {
 			if buttonIndex == 0 {
 				go clipboard.Write(clipboard.FmtText, []byte(message))
 			} else {
-				f.
-					RemovePage(errorPageName).
-					SwitchToPage(formPageName)
+				f.RemoveLayer(errorPageName)
 			}
 		})
+	{
+		bg := f.cfg.Theme.Dialog.Style.GetBackground()
+		if bg != tcell.ColorDefault {
+			modal.SetBackgroundColor(bg)
+			modal.SetButtonBackgroundColor(bg)
+		}
+		fg := f.cfg.Theme.Dialog.Style.GetForeground()
+		if fg != tcell.ColorDefault {
+			modal.SetTextColor(fg)
+			modal.SetButtonTextColor(fg)
+		}
+		// Keep button styles aligned with dialog content without hiding text.
+		modal.SetButtonStyle(f.cfg.Theme.Dialog.Style.Style)
+		modal.SetButtonActivatedStyle(f.cfg.Theme.Dialog.Style.Style)
+	}
 	f.
-		AddAndSwitchToPage(errorPageName, ui.Centered(modal, 0, 0), true).
-		ShowPage(formPageName)
+		AddLayer(
+			ui.Centered(modal, 0, 0),
+			layers.WithName(errorPageName),
+			layers.WithResize(true),
+			layers.WithVisible(true),
+			layers.WithOverlay(),
+		).
+		SendToFront(errorPageName)
 }
 
 func (f *Form) loginWithQR() {
@@ -87,18 +110,18 @@ func (f *Form) loginWithQR() {
 		}
 
 		if token == "" {
-			f.RemovePage(qrPageName).SwitchToPage(formPageName)
+			f.RemoveLayer(qrPageName)
 			return
 		}
 
 		go keyring.SetToken(token)
 
-		f.RemovePage(qrPageName)
+		f.RemoveLayer(qrPageName)
 		if f.done != nil {
 			f.done(token)
 		}
 	})
 
-	f.AddAndSwitchToPage(qrPageName, qr, true)
+	f.AddLayer(qr, layers.WithName(qrPageName), layers.WithResize(true), layers.WithVisible(true))
 	qr.start()
 }