157 lines
5.1 KiB
Go
157 lines
5.1 KiB
Go
package mujica
|
|
|
|
import (
|
|
"cmp"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// ConversationItemOptions configure a ConversationItem.
|
|
type ConversationItemOptions struct {
|
|
// Today decides the time stamp; zero uses the clock.
|
|
Today Date
|
|
Selected bool
|
|
Disabled bool
|
|
// Draggable lets the row be dropped on a ProjectList.
|
|
Draggable bool
|
|
}
|
|
|
|
// ConversationDrag is the value a dragged conversation carries.
|
|
type ConversationDrag struct{ ID string }
|
|
|
|
// ConversationItemView is a built ConversationItem and its requests.
|
|
type ConversationItemView struct {
|
|
Element *ui.Element
|
|
// Opened, Deleted, Exported and PinToggled are requests for the caller.
|
|
Opened, Deleted, Exported, PinToggled bool
|
|
// Renamed reports a confirmed new title, already in conv.Title.
|
|
Renamed bool
|
|
}
|
|
|
|
type conversationItemState struct {
|
|
editing, fresh bool
|
|
draft string
|
|
}
|
|
|
|
type conversationItemKey struct{}
|
|
|
|
// ConversationItem is a conversation row with rename, pin, export, delete.
|
|
func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationItemOptions) ConversationItemView {
|
|
if conv == nil || conv.ID == "" {
|
|
panic("mujica: ConversationItem needs a conversation with an ID")
|
|
}
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
today := opts.Today
|
|
if today.IsZero() {
|
|
today = DateOf(c.Now())
|
|
}
|
|
row := ui.Row(c).Key(conv.ID).FillWidth().MinHeight(d.ControlHeight()).Gap(d.Unit()).AlignItems(ui.Center).
|
|
Radius(theme.ControlRadius).Disabled(opts.Disabled)
|
|
v := ConversationItemView{Element: row}
|
|
st := ui.Local(row, conversationItemKey{}, func() conversationItemState { return conversationItemState{} })
|
|
if row.IsDisabled() {
|
|
st.editing = false
|
|
}
|
|
if opts.Selected {
|
|
row.Background(k.Selection)
|
|
}
|
|
title := cmp.Or(conv.Title, msg(c, "conversation_list.untitled"))
|
|
rename := false
|
|
row.Children(func() {
|
|
if st.editing {
|
|
conversationItemEditor(c, k, conv, st, &v)
|
|
} else {
|
|
main := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).
|
|
Justify(ui.Start).Radius(theme.ControlRadius).Label(title)
|
|
internal.Ring(main, k.Focus, theme.ControlRadius)
|
|
if opts.Draggable {
|
|
main.Drag(ConversationDrag{ID: conv.ID})
|
|
}
|
|
if main.Hovered() && !opts.Selected {
|
|
main.Background(k.SurfaceHover)
|
|
}
|
|
main.Transition(colorFade)
|
|
main.Children(func() {
|
|
if conv.Pinned {
|
|
ui.Icon(c, icons.Must("star")).Size(14, 14).Shrink(0).TextColor(k.AccentText).Label(msg(c, "conversation_item.pinned"))
|
|
}
|
|
t := ui.Text(c, title).SingleLine().Grow(1).MinWidth(0)
|
|
if opts.Selected {
|
|
t.TextColor(k.AccentText)
|
|
}
|
|
ui.Text(c, dateSeparatorWhen(c, conv.Updated, today)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0)
|
|
})
|
|
if !main.IsDisabled() {
|
|
v.Opened = main.Clicked()
|
|
rename = main.Shortcut(0, ui.KeyF2)
|
|
v.Deleted = main.Shortcut(0, ui.KeyDelete)
|
|
}
|
|
}
|
|
conversationItemMenu(c, msg(c, "conversation_item.actions"), func(m *PopupMenu) {
|
|
rename = m.Item(msg(c, "conversation_item.rename"), PopupMenuItemOptions{}) || rename
|
|
pin := msg(c, "conversation_item.pin")
|
|
if conv.Pinned {
|
|
pin = msg(c, "conversation_item.unpin")
|
|
}
|
|
v.PinToggled = m.Item(pin, PopupMenuItemOptions{})
|
|
v.Exported = m.Item(msg(c, "conversation_item.export"), PopupMenuItemOptions{})
|
|
m.Separator()
|
|
v.Deleted = m.Item(msg(c, "conversation_item.delete"), PopupMenuItemOptions{}) || v.Deleted
|
|
})
|
|
})
|
|
if rename && !row.IsDisabled() {
|
|
st.editing, st.fresh, st.draft = true, true, conv.Title
|
|
}
|
|
switch {
|
|
case v.Opened:
|
|
slog.Debug("mujica: conversation_item open", "id", conv.ID)
|
|
case v.Deleted, v.Exported, v.PinToggled:
|
|
slog.Debug("mujica: conversation_item action", "id", conv.ID, "delete", v.Deleted, "export", v.Exported, "pin", v.PinToggled)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// conversationItemEditor edits the title in place; Escape cancels.
|
|
func conversationItemEditor(c *ui.Context, k theme.Tokens, conv *ChatConversation, st *conversationItemState, v *ConversationItemView) {
|
|
d := Density(c)
|
|
in := ui.TextInputBase(c, &st.draft).Grow(1).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2).
|
|
Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.AccentText).
|
|
Label(msg(c, "conversation_item.rename"))
|
|
internal.Ring(in, k.Focus, theme.ControlRadius)
|
|
switch {
|
|
case st.fresh:
|
|
st.fresh = false
|
|
in.Focus()
|
|
case in.IsDisabled():
|
|
case in.Submitted():
|
|
if t := strings.TrimSpace(st.draft); t != "" {
|
|
v.Renamed = t != conv.Title
|
|
conv.Title = t
|
|
}
|
|
st.editing = false
|
|
slog.Debug("mujica: conversation_item rename", "id", conv.ID, "changed", v.Renamed)
|
|
case in.Shortcut(0, ui.KeyEscape):
|
|
st.editing = false
|
|
}
|
|
}
|
|
|
|
// conversationItemMenu is a borderless ellipsis button opening a menu.
|
|
func conversationItemMenu(c *ui.Context, label string, build func(m *PopupMenu)) *ui.Element {
|
|
b := iconAction(c, icons.Must("ellipsis"), label).Role(ui.RoleMenuButton).Shrink(0)
|
|
open := ui.Local(b, conversationItemKey{}, func() bool { return false })
|
|
if b.IsDisabled() {
|
|
*open = false
|
|
} else if b.Clicked() || b.Shortcut(0, ui.KeyDown) {
|
|
*open = true
|
|
}
|
|
popupMenuOpen(c, b, open, build)
|
|
return b
|
|
}
|