213 lines
6.9 KiB
Go
213 lines
6.9 KiB
Go
package mujica
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// ChatProject is a project that holds conversations; Folder groups projects, empty for none.
|
|
type ChatProject struct {
|
|
ID, Name, Folder string
|
|
}
|
|
|
|
// ProjectListOptions configure a ProjectList.
|
|
type ProjectListOptions struct {
|
|
Label string
|
|
Disabled bool
|
|
}
|
|
|
|
// ProjectListState is the chosen project and which folders and projects are open.
|
|
type ProjectListState struct {
|
|
// Selected is the chosen project's ID; empty for none.
|
|
Selected string
|
|
closed map[string]bool
|
|
open map[string]bool
|
|
}
|
|
|
|
// ProjectListView is a built ProjectList and this frame's events.
|
|
type ProjectListView struct {
|
|
Element *ui.Element
|
|
// Changed reports a newly chosen project, in State.Selected.
|
|
Changed bool
|
|
// Opened is a conversation the user opened, empty for none.
|
|
Opened string
|
|
// Moved asks the caller to move conversation MoveID into project MoveTo.
|
|
Moved bool
|
|
MoveID, MoveTo string
|
|
}
|
|
|
|
// ProjectList shows folders of projects and their conversations; drop a ConversationDrag on a project, or use Move to, to file it.
|
|
func ProjectList(c *ui.Context, s *ProjectListState, projects []ChatProject, conversations func(projectID string) []ChatConversation, opts ProjectListOptions) ProjectListView {
|
|
if s == nil || conversations == nil {
|
|
panic("mujica: ProjectList needs a state and conversations")
|
|
}
|
|
if s.closed == nil {
|
|
s.closed, s.open = map[string]bool{}, map[string]bool{}
|
|
}
|
|
d := Density(c)
|
|
col := ui.Column(c).Gap(2).FocusGroup(ui.Vertical).Role(ui.RoleTree).Label(opts.Label).Disabled(opts.Disabled)
|
|
v := ProjectListView{Element: col}
|
|
col.Children(func() {
|
|
seen := map[string]bool{}
|
|
for _, p := range projects {
|
|
if p.Folder == "" || seen[p.Folder] {
|
|
continue
|
|
}
|
|
seen[p.Folder] = true
|
|
open := !s.closed[p.Folder]
|
|
ui.Column(c).Key("folder:" + p.Folder).Children(func() { projectListFolder(c, p.Folder, &open) })
|
|
s.closed[p.Folder] = !open
|
|
if !open {
|
|
continue
|
|
}
|
|
for _, q := range projects {
|
|
if q.Folder == p.Folder {
|
|
projectListProject(c, s, q, projects, conversations, d.Unit()*6, &v)
|
|
}
|
|
}
|
|
}
|
|
for _, p := range projects {
|
|
if p.Folder == "" {
|
|
projectListProject(c, s, p, projects, conversations, 0, &v)
|
|
}
|
|
}
|
|
})
|
|
if col.IsDisabled() {
|
|
v.Changed, v.Opened, v.Moved = false, "", false
|
|
}
|
|
switch {
|
|
case v.Moved:
|
|
slog.Debug("mujica: project_list move", "conversation", v.MoveID, "project", v.MoveTo)
|
|
case v.Changed:
|
|
slog.Debug("mujica: project_list select", "project", s.Selected)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// projectListFolder is a folder heading on a CollapsibleBase trigger, which reports expanded.
|
|
func projectListFolder(c *ui.Context, title string, open *bool) *ui.Element {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
parts := ui.CollapsibleBase(c, open)
|
|
tr := parts.Trigger.MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).
|
|
Radius(theme.ControlRadius).Label(title)
|
|
internal.Ring(tr, k.Focus, theme.ControlRadius)
|
|
if tr.Hovered() {
|
|
tr.Background(k.SurfaceHover)
|
|
}
|
|
chevron := "chevron-right"
|
|
if *open {
|
|
chevron = "chevron-down"
|
|
}
|
|
tr.Children(func() {
|
|
ui.Icon(c, icons.Must(chevron)).Size(14, 14).TextColor(k.TextMuted)
|
|
ui.Icon(c, icons.Must("folder")).Size(16, 16).TextColor(k.Ornament.Mix(k.Text, 0.3))
|
|
ui.Text(c, title).Font(theme.SerifFont).SingleLine()
|
|
})
|
|
return tr
|
|
}
|
|
|
|
func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all []ChatProject, conversations func(string) []ChatConversation, indent float32, v *ProjectListView) {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
convs := conversations(p.ID)
|
|
chosen := s.Selected == p.ID
|
|
var b *ui.Element
|
|
line := ui.Row(c).Key("project:" + p.ID).AlignItems(ui.Center)
|
|
line.Children(func() { b = ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()) })
|
|
b.Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).
|
|
Gap(d.Unit() * 2).Justify(ui.Start).Radius(theme.ControlRadius).Role(ui.RoleTreeItem).Label(p.Name).Selected(chosen)
|
|
b.Background(ui.Transparent).TextColor(k.Text)
|
|
internal.Ring(b, k.Focus, theme.ControlRadius)
|
|
_, over := ui.DragOver[ConversationDrag](b)
|
|
switch {
|
|
case chosen:
|
|
b.Background(k.Selection).TextColor(k.AccentText)
|
|
case b.Hovered():
|
|
b.Background(k.SurfaceHover)
|
|
}
|
|
if over {
|
|
b.DrawOver(func(pt *ui.Painter, r ui.Rect) { internal.Frame(pt, r, 1, 5, k.AccentText) })
|
|
}
|
|
if drop, ok := ui.Drop[ConversationDrag](b); ok && !b.IsDisabled() {
|
|
v.Moved, v.MoveID, v.MoveTo = true, drop.ID, p.ID
|
|
}
|
|
if b.Clicked() && !chosen {
|
|
s.Selected, v.Changed = p.ID, true
|
|
}
|
|
open := s.open[p.ID]
|
|
if !b.IsDisabled() && len(convs) > 0 {
|
|
switch {
|
|
case b.Shortcut(0, ui.KeyRight):
|
|
s.open[p.ID] = true
|
|
case b.Shortcut(0, ui.KeyLeft):
|
|
s.open[p.ID] = false
|
|
}
|
|
}
|
|
if len(convs) > 0 {
|
|
b.Description(msg(c, map[bool]string{true: "project_list.expanded", false: "project_list.collapsed"}[open]))
|
|
}
|
|
b.Children(func() {
|
|
ui.Box(c).Size(8, 8).Shrink(0).Draw(func(pt *ui.Painter, r ui.Rect) {
|
|
if chosen {
|
|
pt.FillPath(internal.Diamond(r.X+4, r.Y+4, 8), k.AccentText)
|
|
}
|
|
})
|
|
ui.Text(c, p.Name).SingleLine().Grow(1).MinWidth(0)
|
|
ui.Text(c, fmt.Sprint(len(convs))).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
|
})
|
|
if len(convs) > 0 {
|
|
chevron := map[bool]string{true: "chevron-down", false: "chevron-right"}[open]
|
|
line.Children(func() {
|
|
t := iconAction(c, icons.Must(chevron), fmt.Sprintf(msg(c, map[bool]string{true: "project_list.collapse", false: "project_list.expand"}[open]), p.Name))
|
|
if t.Clicked() && !t.IsDisabled() {
|
|
s.open[p.ID] = !open
|
|
}
|
|
})
|
|
}
|
|
if !open {
|
|
return
|
|
}
|
|
for _, conv := range convs {
|
|
projectListConversation(c, conv, p, all, indent+d.Unit()*6, v)
|
|
}
|
|
}
|
|
|
|
func projectListConversation(c *ui.Context, conv ChatConversation, home ChatProject, all []ChatProject, indent float32, v *ProjectListView) {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
ui.Row(c).Key("conv:" + conv.ID).AlignItems(ui.Center).Children(func() {
|
|
b := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2, 0, d.Unit()*2+indent).
|
|
Justify(ui.Start).Radius(theme.ControlRadius).Label(conv.Title).Drag(ConversationDrag{ID: conv.ID})
|
|
internal.Ring(b, k.Focus, theme.ControlRadius)
|
|
if b.Dragging() {
|
|
b.Opacity(0.4)
|
|
}
|
|
if b.Hovered() {
|
|
b.Background(k.SurfaceHover)
|
|
}
|
|
b.Children(func() {
|
|
ui.Text(c, cmp.Or(conv.Title, msg(c, "conversation_list.untitled"))).SingleLine().TextColor(k.TextMuted)
|
|
})
|
|
if b.Clicked() {
|
|
v.Opened = conv.ID
|
|
}
|
|
DropdownMenu(c, "", DropdownMenuOptions{Icon: icons.Must("ellipsis"), Label: fmt.Sprintf(msg(c, "project_list.move_to"), conv.Title)}, func(m *PopupMenu) {
|
|
m.Group(msg(c, "project_list.move_group"), func() {
|
|
for _, p := range all {
|
|
if p.ID != home.ID && m.Item(p.Name, PopupMenuItemOptions{}) {
|
|
v.Moved, v.MoveID, v.MoveTo = true, conv.ID, p.ID
|
|
}
|
|
}
|
|
})
|
|
}).Shrink(0)
|
|
})
|
|
}
|