From f0a2e8d5da0e53ebc28db8596eaf71fa1087116c Mon Sep 17 00:00:00 2001 From: Ayyan Date: Wed, 4 Feb 2026 20:49:22 -0500 Subject: [PATCH] feat(ui): add backdrop for dialogs (#736) --- go.mod | 4 +-- go.sum | 8 ++--- internal/config/config.toml | 5 +++ internal/config/theme.go | 6 ++++ internal/ui/chat/message_input.go | 12 ++++--- internal/ui/chat/messages_list.go | 13 ++++++-- internal/ui/chat/view.go | 34 ++++++++++++++------ internal/ui/login/form.go | 53 ++++++++++++++++++++++--------- 8 files changed, 97 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index adac73d..d970c40 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 63e529f..489f5b9 100644 --- a/go.sum +++ b/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= diff --git a/internal/config/config.toml b/internal/config/config.toml index 1acb756..27e5745 100644 --- a/internal/config/config.toml +++ b/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" } diff --git a/internal/config/theme.go b/internal/config/theme.go index caab696..8a59452 100644 --- a/internal/config/theme.go +++ b/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"` } ) diff --git a/internal/ui/chat/message_input.go b/internal/ui/chat/message_input.go index 792534f..39e8717 100644 --- a/internal/ui/chat/message_input.go +++ b/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() { diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index f1e3f03..8cbe704 100644 --- a/internal/ui/chat/messages_list.go +++ b/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) { diff --git a/internal/ui/chat/view.go b/internal/ui/chat/view.go index 552871b..5921135 100644 --- a/internal/ui/chat/view.go +++ b/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) { diff --git a/internal/ui/login/form.go b/internal/ui/login/form.go index 8d6a367..30390cc 100644 --- a/internal/ui/login/form.go +++ b/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() }