Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/sidebar/sidebar.go
366 views
1
// Package sidebar contains the sidebar showing guilds and channels.
2
package sidebar
3
4
import (
5
"context"
6
"log/slog"
7
"strings"
8
9
"github.com/diamondburned/arikawa/v3/discord"
10
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
11
"github.com/diamondburned/gotk4/pkg/gtk/v4"
12
"github.com/diamondburned/gotkit/gtkutil"
13
"github.com/diamondburned/gotkit/gtkutil/cssutil"
14
"libdb.so/dissent/internal/gtkcord"
15
"libdb.so/dissent/internal/sidebar/channels"
16
"libdb.so/dissent/internal/sidebar/direct"
17
"libdb.so/dissent/internal/sidebar/directbutton"
18
"libdb.so/dissent/internal/sidebar/guilds"
19
)
20
21
// Sidebar is the bar on the left side of the application once it's logged in.
22
type Sidebar struct {
23
*gtk.Box // horizontal
24
25
Left *gtk.Box
26
DMView *directbutton.View
27
Guilds *guilds.View
28
Right *gtk.Stack
29
30
// Keep track of the last child to remove.
31
current struct {
32
w gtk.Widgetter
33
// id discord.GuildID
34
}
35
placeholder gtk.Widgetter
36
37
ctx context.Context
38
}
39
40
var sidebarCSS = cssutil.Applier("sidebar-sidebar", `
41
@define-color sidebar_bg mix(@borders, @theme_bg_color, 0.25);
42
43
.sidebar-guildside {
44
background-color: @sidebar_bg;
45
}
46
.sidebar-guildside windowcontrols:not(.empty) {
47
margin-left: 4px;
48
margin-right: 4px;
49
}
50
.sidebar-guildside windowcontrols:not(.empty) button {
51
margin: 0px 0;
52
}
53
`)
54
55
// NewSidebar creates a new Sidebar.
56
func NewSidebar(ctx context.Context) *Sidebar {
57
s := Sidebar{
58
ctx: ctx,
59
}
60
61
s.Guilds = guilds.NewView(ctx)
62
s.Guilds.Invalidate()
63
64
s.DMView = directbutton.NewView(ctx)
65
s.DMView.Invalidate()
66
67
dmSeparator := gtk.NewSeparator(gtk.OrientationHorizontal)
68
dmSeparator.AddCSSClass("sidebar-dm-separator")
69
70
// leftBox holds just the DM button and the guild view, as opposed to s.Left
71
// which holds the scrolled window and the window controls.
72
leftBox := gtk.NewBox(gtk.OrientationVertical, 0)
73
leftBox.Append(s.DMView)
74
leftBox.Append(dmSeparator)
75
leftBox.Append(s.Guilds)
76
77
leftScroll := gtk.NewScrolledWindow()
78
leftScroll.SetVExpand(true)
79
leftScroll.SetPolicy(gtk.PolicyNever, gtk.PolicyExternal)
80
leftScroll.SetChild(leftBox)
81
82
leftCtrl := gtk.NewWindowControls(gtk.PackStart)
83
leftCtrl.SetHAlign(gtk.AlignCenter)
84
85
s.Left = gtk.NewBox(gtk.OrientationVertical, 0)
86
s.Left.AddCSSClass("sidebar-guildside")
87
s.Left.Append(leftCtrl)
88
s.Left.Append(leftScroll)
89
90
s.placeholder = gtk.NewWindowHandle()
91
92
s.Right = gtk.NewStack()
93
s.Right.SetSizeRequest(channels.ChannelsWidth, -1)
94
s.Right.SetVExpand(true)
95
s.Right.SetHExpand(true)
96
s.Right.AddChild(s.placeholder)
97
s.Right.SetVisibleChild(s.placeholder)
98
s.Right.SetTransitionType(gtk.StackTransitionTypeCrossfade)
99
100
userBar := newUserBar(ctx, []gtkutil.PopoverMenuItem{
101
gtkutil.MenuItem("Quick Switcher", "win.quick-switcher"),
102
gtkutil.MenuSeparator("User Settings"),
103
gtkutil.Submenu("Set _Status", []gtkutil.PopoverMenuItem{
104
gtkutil.MenuItem("_Online", "win.set-online"),
105
gtkutil.MenuItem("_Idle", "win.set-idle"),
106
gtkutil.MenuItem("_Do Not Disturb", "win.set-dnd"),
107
gtkutil.MenuItem("In_visible", "win.set-invisible"),
108
}),
109
gtkutil.MenuSeparator(""),
110
gtkutil.MenuItem("_Preferences", "app.preferences"),
111
gtkutil.MenuItem("_About", "app.about"),
112
gtkutil.MenuItem("_Logs", "app.logs"),
113
gtkutil.MenuItem("_Quit", "app.quit"),
114
})
115
116
// TODO: consider if we can merge this ToolbarView with the one in channels
117
// and direct.
118
rightWrap := adw.NewToolbarView()
119
rightWrap.AddBottomBar(userBar)
120
rightWrap.SetContent(s.Right)
121
122
s.Box = gtk.NewBox(gtk.OrientationHorizontal, 0)
123
s.Box.SetHExpand(false)
124
s.Box.Append(s.Left)
125
s.Box.Append(rightWrap)
126
sidebarCSS(s)
127
128
return &s
129
}
130
131
// GuildID returns the guild ID that the channel list is showing for, if any.
132
// If not, 0 is returned.
133
func (s *Sidebar) GuildID() discord.GuildID {
134
ch, ok := s.current.w.(*channels.View)
135
if !ok {
136
return 0
137
}
138
return ch.GuildID()
139
}
140
141
func (s *Sidebar) stackSelect(w gtk.Widgetter) {
142
if w == s.current.w {
143
return
144
}
145
146
old := s.current.w
147
s.current.w = w
148
149
if w == nil {
150
s.Right.SetVisibleChild(s.placeholder)
151
} else {
152
// This should do nothing if the widget is already in the stack.
153
// Maybe???
154
s.Right.AddChild(w)
155
s.Right.SetVisibleChild(w)
156
157
w := gtk.BaseWidget(w)
158
w.GrabFocus()
159
}
160
161
if old != nil {
162
gtkutil.NotifyProperty(s.Right, "transition-running", func() bool {
163
// Remove the widget when the transition is done.
164
if !s.Right.TransitionRunning() {
165
s.Right.Remove(old)
166
167
w := gtk.BaseWidget(old)
168
slog.Debug(
169
"sidebar: right stack transition done, removed widget",
170
"widget", w.Type().String()+"."+strings.Join(w.CSSClasses(), "."))
171
172
return true
173
} else {
174
slog.Debug("sidebar: right stack transition started")
175
return false
176
}
177
})
178
}
179
}
180
181
// OpenDMs opens the DMs view. It automatically loads the DMs on first open, so
182
// the returned ChannelView is guaranteed to be ready.
183
func (s *Sidebar) OpenDMs() *direct.ChannelView {
184
if direct, ok := s.current.w.(*direct.ChannelView); ok {
185
// we're already there
186
return direct
187
}
188
189
s.unselect()
190
s.DMView.SetSelected(true)
191
192
direct := direct.NewChannelView(s.ctx)
193
direct.SetVExpand(true)
194
direct.Invalidate()
195
196
s.stackSelect(direct)
197
198
return direct
199
}
200
201
func (s *Sidebar) openGuild(guildID discord.GuildID) *channels.View {
202
chs, ok := s.current.w.(*channels.View)
203
if ok && chs.GuildID() == guildID {
204
// We're already there.
205
return chs
206
}
207
208
s.unselect()
209
s.Guilds.SetSelectedGuild(guildID)
210
211
chs = channels.NewView(s.ctx, guildID)
212
chs.SetVExpand(true)
213
chs.InvalidateHeader()
214
215
s.stackSelect(chs)
216
return chs
217
}
218
219
func (s *Sidebar) unselect() {
220
s.Guilds.Unselect()
221
s.DMView.Unselect()
222
s.stackSelect(nil)
223
}
224
225
// Unselect unselects the current guild or channel.
226
func (s *Sidebar) Unselect() {
227
s.unselect()
228
s.Right.SetVisibleChild(s.placeholder)
229
}
230
231
// SetSelectedGuild marks the guild with the given ID as selected.
232
func (s *Sidebar) SetSelectedGuild(guildID discord.GuildID) {
233
s.Guilds.SetSelectedGuild(guildID)
234
s.openGuild(guildID)
235
}
236
237
// // SelectGuild selects and activates the guild with the given ID.
238
// func (s *Sidebar) SelectGuild(guildID discord.GuildID) {
239
// if s.Guilds.SelectedGuildID() != guildID {
240
// s.Guilds.SetSelectedGuild(guildID)
241
//
242
// parent := gtk.BaseWidget(s.Parent())
243
// parent.ActivateAction("win.open-guild", gtkcord.NewGuildIDVariant(guildID))
244
// }
245
// }
246
247
// SelectChannel selects and activates the channel with the given ID. It ensures
248
// that the sidebar is at the right place then activates the controller.
249
// This function acts the same as if the user clicked on the channel, meaning it
250
// funnels down to a single widget that then floats up to the controller.
251
func (s *Sidebar) SelectChannel(chID discord.ChannelID) {
252
state := gtkcord.FromContext(s.ctx)
253
ch, _ := state.Cabinet.Channel(chID)
254
if ch == nil {
255
slog.Error(
256
"cannot select channel in sidebar since it's not found in state",
257
"channel_id", chID)
258
return
259
}
260
261
s.Guilds.SetSelectedGuild(ch.GuildID)
262
263
if ch.GuildID.IsValid() {
264
guild := s.openGuild(ch.GuildID)
265
guild.SelectChannel(chID)
266
} else {
267
direct := s.OpenDMs()
268
direct.SelectChannel(chID)
269
}
270
}
271
272