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

This commit is contained in:
Ayyan 2026-02-04 20:49:22 -05:00 • committed by GitHub
parent 5f884cdf28
commit f0a2e8d5da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 38 deletions

4
go.mod
View file

@ -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

8
go.sum
View file

@ -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=

View file

@ -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" }

View file

@ -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"`
}
)

View file

@ -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() {

View file

@ -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) {

View file

@ -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) {

View file

@ -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()
}