feat(messageslist): show custom emoji images in reactions, wrapped with a separator
This commit is contained in:
parent
805621bc2e
commit
997a77c033
16 changed files with 208 additions and 54 deletions
|
|
@ -424,6 +424,20 @@ The longest time between two messages by the same author for them to share a hea
|
|||
|
||||
Default: `"10m0s"`
|
||||
|
||||
### ui.messages_list.reactions
|
||||
|
||||
#### ui.messages_list.reactions.separator
|
||||
|
||||
The text between reactions.
|
||||
|
||||
Default: `" • "`
|
||||
|
||||
#### ui.messages_list.reactions.colons_around_name
|
||||
|
||||
Whether the names of custom emoji are shown between colons, as in `:name:`.
|
||||
|
||||
Default: `false`
|
||||
|
||||
## ui.mentions_list
|
||||
|
||||
The list of mention suggestions above the composer. Its width and height are capped to the available space.
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -16,7 +16,7 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/ayn2op/arikawa/v3 v3.0.0-20261016043052-54a68a54e42e
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f
|
||||
github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5
|
||||
github.com/bogdanfinn/fhttp v0.6.9
|
||||
github.com/bogdanfinn/tls-client v1.16.0
|
||||
github.com/gdamore/tcell/v3 v3.6.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -16,8 +16,8 @@ github.com/ayn2op/fhttp v0.6.10-0.20261016015330-2d5e34c6b935 h1:oVxkY1/nVsVQ8Cn
|
|||
github.com/ayn2op/fhttp v0.6.10-0.20261016015330-2d5e34c6b935/go.mod h1:7phoj3f/q54WuiTnCTlAhEoaVcdlruKkSzCSD47LzHk=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426 h1:0ndRWpAiSSgR0z9b7K+pwMlCHEk3VzQeS5ls+zWtbr0=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426/go.mod h1:4S2B9frwrjELeAldoqPgasA/VfnOySgRlektwYLuVks=
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f h1:f3Kiqss18xrNTzyB5MfzVTGfrX1tUwMPENmku8BdNGg=
|
||||
github.com/ayn2op/tview v0.0.0-20261017014958-0756c1611d0f/go.mod h1:kn8Up2Tk2vhDUtW6H9nqAvRk9hDVyCzk18CCAo2qkjA=
|
||||
github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5 h1:IbG2cMS8rH6fEtktBc/k+HDyIt0Dosx+pB4sxtXXpRM=
|
||||
github.com/ayn2op/tview v0.0.0-20261017195448-33f4b52a0ef5/go.mod h1:kn8Up2Tk2vhDUtW6H9nqAvRk9hDVyCzk18CCAo2qkjA=
|
||||
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ func (e enum[T]) text(value T) ([]byte, error) {
|
|||
var (
|
||||
boxDrawing = scrollbar.SymbolSet{Track: "│", Thumb: "█", Begin: "▲", End: "▼"}
|
||||
|
||||
alignments = enum[tview.Alignment]{
|
||||
{"left", tview.AlignmentLeft},
|
||||
{"center", tview.AlignmentCenter},
|
||||
{"right", tview.AlignmentRight},
|
||||
alignments = enum[tview.HorizontalAlignment]{
|
||||
{"left", tview.HorizontalAlignmentLeft},
|
||||
{"center", tview.HorizontalAlignmentCenter},
|
||||
{"right", tview.HorizontalAlignmentRight},
|
||||
}
|
||||
borderSets = enum[tview.BorderSet]{
|
||||
{"hidden", tview.BorderSetHidden()},
|
||||
|
|
@ -67,14 +67,16 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
type AlignmentWrapper struct{ tview.Alignment }
|
||||
type AlignmentWrapper struct{ tview.HorizontalAlignment }
|
||||
|
||||
func (aw *AlignmentWrapper) UnmarshalText(text []byte) (err error) {
|
||||
aw.Alignment, err = alignments.parse(string(text))
|
||||
aw.HorizontalAlignment, err = alignments.parse(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (aw AlignmentWrapper) MarshalText() ([]byte, error) { return alignments.text(aw.Alignment) }
|
||||
func (aw AlignmentWrapper) MarshalText() ([]byte, error) {
|
||||
return alignments.text(aw.HorizontalAlignment)
|
||||
}
|
||||
|
||||
type BorderSetWrapper struct{ tview.BorderSet }
|
||||
|
||||
|
|
@ -181,6 +183,14 @@ type (
|
|||
Attachments AttachmentsUIConfig `toml:"attachments"`
|
||||
Avatars AvatarsUIConfig `toml:"avatars"`
|
||||
Grouping GroupingUIConfig `toml:"grouping"`
|
||||
Reactions ReactionsUIConfig `toml:"reactions"`
|
||||
}
|
||||
|
||||
ReactionsUIConfig struct {
|
||||
// The text between reactions.
|
||||
Separator string `toml:"separator"`
|
||||
// Whether the names of custom emoji are shown between colons, as in `:name:`.
|
||||
ColonsAroundName bool `toml:"colons_around_name"`
|
||||
}
|
||||
|
||||
// GroupingUIConfig shows consecutive messages by the same author under one header.
|
||||
|
|
@ -214,7 +224,7 @@ type (
|
|||
)
|
||||
|
||||
func defaultUI() UIConfig {
|
||||
left := TitleUIConfig{AlignmentWrapper{tview.AlignmentLeft}}
|
||||
left := TitleUIConfig{AlignmentWrapper{tview.HorizontalAlignmentLeft}}
|
||||
round := BorderSetWrapper{tview.BorderSetRound()}
|
||||
attachments := AttachmentsUIConfig{
|
||||
ShowLinks: true,
|
||||
|
|
@ -223,7 +233,7 @@ func defaultUI() UIConfig {
|
|||
}
|
||||
return UIConfig{
|
||||
Tabs: TabsUIConfig{
|
||||
Alignment: AlignmentWrapper{tview.AlignmentCenter},
|
||||
Alignment: AlignmentWrapper{tview.HorizontalAlignmentCenter},
|
||||
Separator: "│",
|
||||
Padding: [2]string{" ", " "},
|
||||
Arrows: [2]string{"◀", "▶"},
|
||||
|
|
@ -246,6 +256,7 @@ func defaultUI() UIConfig {
|
|||
Attachments: attachments,
|
||||
Avatars: AvatarsUIConfig{Preview: true, Height: 2},
|
||||
Grouping: GroupingUIConfig{Enabled: true, Interval: 10 * time.Minute},
|
||||
Reactions: ReactionsUIConfig{Separator: " • "},
|
||||
},
|
||||
MentionsList: MentionsListUIConfig{MinWidth: 20},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ func (r row) Draw(screen tview.Screen, area tview.Rectangle) {
|
|||
for x := area.X; x < area.X+area.Width; x++ {
|
||||
screen.Put(x, area.Y, " ", r.style)
|
||||
}
|
||||
tview.Print(screen, r.text, area.X, area.Y, area.Width, tview.AlignmentLeft, r.style)
|
||||
tview.Print(screen, r.text, area.X, area.Y, area.Width, tview.HorizontalAlignmentLeft, r.style)
|
||||
}
|
||||
|
||||
func (row) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg }
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/ayn2op/discordo/internal/markdown"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"github.com/ayn2op/tview/row"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/color"
|
||||
|
|
@ -89,7 +90,7 @@ func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) {
|
|||
fill := area.Width - labelWidth
|
||||
line = strings.Repeat(d.fill, fill/2) + label + strings.Repeat(d.fill, fill-fill/2)
|
||||
}
|
||||
tview.Print(screen, line, area.X, area.Y, area.Width, tview.AlignmentLeft, d.style)
|
||||
tview.Print(screen, line, area.X, area.Y, area.Width, tview.HorizontalAlignmentLeft, d.style)
|
||||
}
|
||||
|
||||
func (dateSeparator) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg }
|
||||
|
|
@ -134,30 +135,42 @@ func (m Model) writeMessage(builder *markdown.Builder, message discord.Message,
|
|||
default:
|
||||
m.drawLayout(builder, message, baseStyle, false, nil)
|
||||
}
|
||||
m.drawReactions(builder, message.Reactions, baseStyle)
|
||||
}
|
||||
|
||||
func (m Model) drawReactions(builder *markdown.Builder, reactions []discord.Reaction, baseStyle tview.Style) {
|
||||
// reactionsView returns the reactions wrapped to the width of the message, with the image of each custom emoji that has one before its name, or nil if there are none.
|
||||
func (m Model) reactionsView(reactions []discord.Reaction) tview.Widget {
|
||||
if len(reactions) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
builder.NewLine()
|
||||
theme, cfg := m.cfg.Theme.MessagesList, m.cfg.UI.MessagesList.Reactions
|
||||
separatorStyle := tview.MergeStyle(theme.MessageStyle.Style, theme.ReactionStyle.Style)
|
||||
chips := make([]tview.Widget, len(reactions))
|
||||
for i, reaction := range reactions {
|
||||
if i > 0 {
|
||||
builder.Write(" ", baseStyle)
|
||||
style := theme.ReactionStyle.Style
|
||||
if reaction.Me {
|
||||
style = theme.OwnReactionStyle.Style
|
||||
}
|
||||
style = tview.MergeStyle(theme.MessageStyle.Style, style)
|
||||
|
||||
name := reaction.Emoji.Name
|
||||
if reaction.Emoji.IsCustom() {
|
||||
if reaction.Emoji.IsCustom() && cfg.ColonsAroundName {
|
||||
name = ":" + name + ":"
|
||||
}
|
||||
style := m.cfg.Theme.MessagesList.ReactionStyle.Style
|
||||
if reaction.Me {
|
||||
style = m.cfg.Theme.MessagesList.OwnReactionStyle.Style
|
||||
|
||||
spans := []text.Span{{Text: name + " " + strconv.Itoa(reaction.Count), Style: style}}
|
||||
// Each reaction but the last ends with the separator, so a line never starts with one.
|
||||
if i < len(reactions)-1 {
|
||||
spans = append(spans, text.Span{Text: cfg.Separator, Style: separatorStyle})
|
||||
}
|
||||
builder.Write(name+" "+strconv.Itoa(reaction.Count), tview.MergeStyle(baseStyle, style))
|
||||
|
||||
var chip tview.Widget = text.Rich(spans...)
|
||||
if emoji, ok := m.images[emojiURL(reaction.Emoji)]; ok {
|
||||
chip = row.New(emoji, chip).Spacing(1).Width(layout.Shrink).Height(layout.Shrink)
|
||||
}
|
||||
chips[i] = chip
|
||||
}
|
||||
return row.New(chips...).Height(layout.Shrink).Wrap()
|
||||
}
|
||||
|
||||
func (m Model) formatTimestamp(ts discord.Timestamp) string {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ type Model struct {
|
|||
items []messageItem
|
||||
|
||||
renderer *markdown.Renderer
|
||||
avatars map[discord.URL]tviewimage.Widget
|
||||
// images are the avatars and emoji, shared by every message that shows them.
|
||||
images map[discord.URL]tviewimage.Widget
|
||||
// voicePlayer plays the voice message played last, and voiceStatus is what it last said of it.
|
||||
voicePlayer *voice.Player
|
||||
voiceStatus voice.PlayerStatus
|
||||
|
|
@ -69,7 +70,7 @@ func NewModel(cfg *config.Config, state *ningen.State) Model {
|
|||
cfg: cfg,
|
||||
state: state,
|
||||
renderer: markdown.NewRenderer(cfg),
|
||||
avatars: map[discord.URL]tviewimage.Widget{},
|
||||
images: map[discord.URL]tviewimage.Widget{},
|
||||
voicePlayer: voice.NewPlayer(),
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +156,14 @@ func (m *Model) UpdateMessage(message discord.Message) tview.Cmd {
|
|||
}
|
||||
|
||||
// RefreshMessage shows the cached state of a message, such as after a reaction.
|
||||
func (m *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) {
|
||||
if message, err := m.state.Cabinet.Message(channelID, id); err == nil {
|
||||
m.setMessage(m.indexOf(channelID, id), *message)
|
||||
func (m *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) tview.Cmd {
|
||||
message, err := m.state.Cabinet.Message(channelID, id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.setMessage(m.indexOf(channelID, id), *message)
|
||||
return m.loadPreviews()
|
||||
}
|
||||
|
||||
// DeleteMessage removes the shown message with id.
|
||||
|
|
@ -176,7 +181,7 @@ func (m Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int {
|
|||
|
||||
func (m *Model) reset() {
|
||||
m.items = nil
|
||||
clear(m.avatars)
|
||||
clear(m.images)
|
||||
m.cursor, m.target = -1, -1
|
||||
m.scrollState = viewport.ScrollState{}
|
||||
m.scrollState.ScrollToEnd()
|
||||
|
|
@ -266,21 +271,26 @@ func (m Model) buildItem(index int) tview.Widget {
|
|||
}
|
||||
}
|
||||
|
||||
if avatar, ok := m.avatars[avatarURL(message.Author)]; ok {
|
||||
if !m.blocked(message.Author) {
|
||||
body = body.Push(m.reactionsView(message.Reactions))
|
||||
}
|
||||
|
||||
var view tview.Widget = body
|
||||
if avatar, ok := m.images[avatarURL(message.Author)]; ok {
|
||||
var gutter tview.Widget = avatar
|
||||
// A grouped message leaves the avatar's column empty, under the avatar of the group.
|
||||
if item.grouped {
|
||||
width, _ := avatar.Size()
|
||||
gutter = spacer.New().Width(width)
|
||||
}
|
||||
body = row.New(gutter, body).Spacing(1).Height(layout.Shrink)
|
||||
view = row.New(gutter, body).Spacing(1).Height(layout.Shrink)
|
||||
}
|
||||
|
||||
if message.Type == discord.InlinedReplyMessage && !m.blocked(message.Author) {
|
||||
body = column.New(item.reply, body).Height(layout.Shrink)
|
||||
view = column.New(item.reply, view).Height(layout.Shrink)
|
||||
}
|
||||
|
||||
return body
|
||||
return view
|
||||
}
|
||||
|
||||
// shouldGroup reports whether message is drawn under the header of previous, the message before it, which is nil for the first.
|
||||
|
|
@ -447,8 +457,8 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
|||
cmd = preview.Transmit()
|
||||
}
|
||||
|
||||
if _, ok := m.avatars[msg.proxy]; ok {
|
||||
m.avatars[msg.proxy] = preview
|
||||
if _, ok := m.images[msg.proxy]; ok {
|
||||
m.images[msg.proxy] = preview
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package messageslist
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -13,8 +14,10 @@ import (
|
|||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
tviewimage "github.com/ayn2op/tview/image"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/vt"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) *config.Config {
|
||||
|
|
@ -353,3 +356,46 @@ func TestModelScrollToTop(t *testing.T) {
|
|||
t.Fatalf("scrolling to the new top fetched %d times, want once", fetches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelReactionsView(t *testing.T) {
|
||||
ml := newTestModel(t, testConfig(t))
|
||||
custom := discord.Emoji{ID: 5, Name: "cat"}
|
||||
reactions := []discord.Reaction{{Count: 2, Emoji: discord.Emoji{Name: "ok"}}, {Count: 1, Emoji: custom}}
|
||||
draw := func() string {
|
||||
screen, err := tcell.NewTerminfoScreenFromTty(vt.NewMockTerm(vt.MockOptSize{X: 20, Y: 1}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := screen.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer screen.Fini()
|
||||
|
||||
ml.reactionsView(reactions).Draw(screen, tview.Rectangle{Width: 20, Height: 1})
|
||||
var row strings.Builder
|
||||
for x := range 20 {
|
||||
str, _, _ := screen.Get(x, 0)
|
||||
row.WriteString(str)
|
||||
}
|
||||
return strings.TrimRight(row.String(), " ")
|
||||
}
|
||||
|
||||
t.Run("names", func(t *testing.T) {
|
||||
if got := draw(); got != "ok 2 • cat 1" {
|
||||
t.Fatalf("reactions = %q", got)
|
||||
}
|
||||
})
|
||||
t.Run("emoji image", func(t *testing.T) {
|
||||
ml.images[emojiURL(custom)] = tviewimage.New(image.NewAlpha(image.Rect(0, 0, 2, 2))).Width(2)
|
||||
// The blank image takes two cells before the name.
|
||||
if got := draw(); got != "ok 2 • cat 1" {
|
||||
t.Fatalf("reactions = %q", got)
|
||||
}
|
||||
})
|
||||
t.Run("with colons", func(t *testing.T) {
|
||||
ml.cfg.UI.MessagesList.Reactions.ColonsAroundName = true
|
||||
if got := draw(); got != "ok 2 • :cat: 1" {
|
||||
t.Fatalf("reactions = %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package messageslist
|
|||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
|
@ -86,6 +87,10 @@ func (m Model) loadPreviews() tview.Cmd {
|
|||
}
|
||||
|
||||
cmds = append(cmds, m.loadAvatar(item.message.Author))
|
||||
for _, reaction := range item.message.Reactions {
|
||||
cmds = append(cmds, m.loadEmoji(reaction.Emoji))
|
||||
}
|
||||
|
||||
if !m.cfg.UI.MessagesList.Attachments.Preview {
|
||||
continue
|
||||
}
|
||||
|
|
@ -116,21 +121,55 @@ func avatarURL(user discord.User) discord.URL {
|
|||
}
|
||||
|
||||
func (m Model) loadAvatar(user discord.User) tview.Cmd {
|
||||
url, cfg := avatarURL(user), m.cfg.UI.MessagesList.Avatars
|
||||
if _, ok := m.avatars[url]; ok || !cfg.Preview || cfg.Height == 0 || m.blocked(user) {
|
||||
cfg := m.cfg.UI.MessagesList.Avatars
|
||||
if !cfg.Preview || cfg.Height == 0 || m.blocked(user) {
|
||||
return nil
|
||||
}
|
||||
return m.loadImage(avatarURL(user), uint(cfg.Height))
|
||||
}
|
||||
|
||||
func emojiURL(emoji discord.Emoji) discord.URL {
|
||||
return emoji.EmojiURLWithType(discord.PNGImage)
|
||||
}
|
||||
|
||||
// loadEmoji loads the image of a custom emoji, a row tall.
|
||||
func (m Model) loadEmoji(emoji discord.Emoji) tview.Cmd {
|
||||
if !emoji.IsCustom() {
|
||||
return nil
|
||||
}
|
||||
return m.loadImage(emojiURL(emoji), 1)
|
||||
}
|
||||
|
||||
// loadImage loads the image at url in a square rows tall, unless it is loaded already.
|
||||
func (m Model) loadImage(url discord.URL, rows uint) tview.Cmd {
|
||||
if _, ok := m.images[url]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cellWidth, cellHeight := m.previewCell()
|
||||
cols := uint(cfg.Height) * cellHeight / cellWidth
|
||||
cols := rows * cellHeight / cellWidth
|
||||
bounds := image.Rect(0, 0, int(cols*cellWidth), int(cols*cellWidth))
|
||||
widget := func(img image.Image) tviewimage.Widget {
|
||||
return tviewimage.New(img).Width(int(cols)).CellSize(int(cellWidth), int(cellHeight))
|
||||
return tviewimage.New(square(img)).Width(int(cols)).CellSize(int(cellWidth), int(cellHeight))
|
||||
}
|
||||
m.avatars[url] = widget(image.NewAlpha(bounds))
|
||||
m.images[url] = widget(image.NewAlpha(bounds))
|
||||
return m.loadPreview(0, url, bounds, widget)
|
||||
}
|
||||
|
||||
// square returns img centered on a transparent square, so an image of any shape takes the square's cells.
|
||||
func square(img image.Image) image.Image {
|
||||
b := img.Bounds()
|
||||
if b.Dx() == b.Dy() {
|
||||
return img
|
||||
}
|
||||
|
||||
side := max(b.Dx(), b.Dy())
|
||||
dst := image.NewNRGBA(image.Rect(0, 0, side, side))
|
||||
at := image.Pt((side-b.Dx())/2, (side-b.Dy())/2)
|
||||
draw.Draw(dst, b.Sub(b.Min).Add(at), img, b.Min, draw.Src)
|
||||
return dst
|
||||
}
|
||||
|
||||
// loadPreview yields an empty preview if loading fails.
|
||||
func (m Model) loadPreview(messageID discord.MessageID, proxy discord.URL, bounds image.Rectangle, widget func(image.Image) tviewimage.Widget) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package messageslist
|
||||
|
||||
import (
|
||||
"image"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
|
|
@ -56,3 +57,23 @@ func TestPreviewSourceCols(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSquare(t *testing.T) {
|
||||
t.Run("square", func(t *testing.T) {
|
||||
img := image.NewAlpha(image.Rect(0, 0, 20, 20))
|
||||
if got := square(img); got != image.Image(img) {
|
||||
t.Fatal("square image was copied")
|
||||
}
|
||||
})
|
||||
t.Run("tall", func(t *testing.T) {
|
||||
img := image.NewAlpha(image.Rect(0, 0, 10, 30))
|
||||
img.Pix[0] = 0xff
|
||||
got := square(img)
|
||||
if got.Bounds() != image.Rect(0, 0, 30, 30) {
|
||||
t.Fatalf("bounds = %v, want 30 by 30", got.Bounds())
|
||||
}
|
||||
if _, _, _, a := got.At(10, 0).RGBA(); a == 0 {
|
||||
t.Fatal("image is not centered")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,15 +116,15 @@ func (m *Model) applyEvent(event gateway.Event) tview.Cmd {
|
|||
case *gateway.MessageDeleteEvent:
|
||||
m.messagesList.DeleteMessage(event.ChannelID, event.ID)
|
||||
case *gateway.MessageReactionAddEvent:
|
||||
m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
case *gateway.MessageReactionAddManyEvent:
|
||||
m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
case *gateway.MessageReactionRemoveEvent:
|
||||
m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
case *gateway.MessageReactionRemoveAllEvent:
|
||||
m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
case *gateway.MessageReactionRemoveEmojiEvent:
|
||||
m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
return m.messagesList.RefreshMessage(event.ChannelID, event.MessageID)
|
||||
|
||||
case *gateway.GuildMembersChunkEvent:
|
||||
return m.refreshMemberNames(event)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func (m Model) View() tview.Widget {
|
|||
func (m Model) tabs() tabs.Widget {
|
||||
cfg := m.cfg.UI.Tabs
|
||||
return tabs.New(m.password.Label(), m.qr.Label(), m.token.Label()).
|
||||
Alignment(cfg.Alignment.Alignment).
|
||||
Alignment(cfg.Alignment.HorizontalAlignment).
|
||||
Wrap(cfg.Wrap).
|
||||
Separator(cfg.Separator).
|
||||
Padding(cfg.Padding[0], cfg.Padding[1]).
|
||||
|
|
|
|||
|
|
@ -82,5 +82,5 @@ func (m Model) View() tview.Widget {
|
|||
row.New(column.New(text.New("Login")).Width(labelWidth), login).Height(layout.Shrink),
|
||||
row.New(column.New(text.New("Password")).Width(labelWidth), password).Height(layout.Shrink),
|
||||
submit,
|
||||
).Spacing(1).Align(tview.AlignmentCenter)
|
||||
).Spacing(1).AlignX(tview.HorizontalAlignmentCenter)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ package qr
|
|||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/center"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"github.com/ayn2op/tview/text"
|
||||
"github.com/ayn2op/tview/viewport"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
|
|
@ -131,7 +131,7 @@ func halfBlock(top, bottom bool) rune {
|
|||
func (m Model) View() tview.Widget {
|
||||
code := m.code()
|
||||
return center.New(
|
||||
viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.AlignmentCenter), m.scrollState).
|
||||
viewport.New(text.New(code).Wrapping(text.WrappingNone).AlignX(tview.HorizontalAlignmentCenter), m.scrollState).
|
||||
Height(layout.Shrink).
|
||||
Focused(true).
|
||||
OnChange(func(s *viewport.ScrollState) tview.Msg { return scrollMsg(*s) }),
|
||||
|
|
|
|||
|
|
@ -62,5 +62,5 @@ func (m Model) View() tview.Widget {
|
|||
return column.New(
|
||||
row.New(column.New(text.New("Token")).Width(layout.Fixed(len("Token")+1)), input).Height(layout.Shrink),
|
||||
submit,
|
||||
).Spacing(1).Align(tview.AlignmentCenter)
|
||||
).Spacing(1).AlignX(tview.HorizontalAlignmentCenter)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ func Box(child tview.Widget, cfg *config.Config, focused bool) box.Widget {
|
|||
padding := ui.Border.Padding
|
||||
b := box.New(child).
|
||||
Padding(padding[0], padding[1], padding[2], padding[3]).
|
||||
TitleAlignment(ui.Title.Alignment.Alignment).
|
||||
FooterAlignment(ui.Footer.Alignment.Alignment)
|
||||
TitleAlignment(ui.Title.Alignment.HorizontalAlignment).
|
||||
FooterAlignment(ui.Footer.Alignment.HorizontalAlignment)
|
||||
if ui.Border.Enabled {
|
||||
b = b.Borders(tview.BordersAll)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue