WIP components (as built)
This commit is contained in:
96 files changed
+6468
-16
No files matched your search
+183
@@ -0,0 +1,183 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// RecentFile is a file the user opened.
|
||||
type RecentFile struct {
|
||||
Path string
|
||||
Opened time.Time
|
||||
}
|
||||
|
||||
// RecentFilesOptions configure a RecentFiles.
|
||||
type RecentFilesOptions struct {
|
||||
// Revision changes when files are edited in place.
|
||||
Revision uint64
|
||||
// Now dates the groups; zero uses the frame's time.
|
||||
Now time.Time
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// RecentFilesState is a RecentFiles' choice and place.
|
||||
type RecentFilesState struct {
|
||||
rows ui.ListState
|
||||
selected int
|
||||
flat []recentRow
|
||||
data *RecentFile
|
||||
n int
|
||||
day time.Time
|
||||
started bool
|
||||
revision uint64
|
||||
}
|
||||
|
||||
// recentRow holds a group header or file index.
|
||||
type recentRow struct{ group, file int }
|
||||
|
||||
// RecentFilesResult is the list and what happened this frame.
|
||||
type RecentFilesResult struct {
|
||||
Element *ui.Element
|
||||
Opened *RecentFile
|
||||
// Removed identifies one file; Cleared requests clearing all files.
|
||||
Removed string
|
||||
Cleared bool
|
||||
}
|
||||
|
||||
// RecentFiles groups recent files by time and supports clearing.
|
||||
func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts RecentFilesOptions) RecentFilesResult {
|
||||
if s == nil {
|
||||
panic("mujica: RecentFiles needs a state")
|
||||
}
|
||||
if !s.started {
|
||||
s.started, s.selected = true, -1
|
||||
}
|
||||
now := opts.Now
|
||||
if now.IsZero() {
|
||||
now = c.Now()
|
||||
}
|
||||
if s.revision != opts.Revision {
|
||||
s.day = time.Time{}
|
||||
s.revision = opts.Revision
|
||||
}
|
||||
recentGroup(files, s, now)
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
var res RecentFilesResult
|
||||
res.Element = codePanel(c).Disabled(opts.Disabled)
|
||||
disabled := res.Element.IsDisabled()
|
||||
if disabled {
|
||||
copy := *s
|
||||
s = ©
|
||||
}
|
||||
res.Element.Children(func() {
|
||||
codeHeader(c, func() {
|
||||
ui.Text(c, msg(c, "recent_files.title")).Font(theme.SerifFont).FontWeight(600).Grow(1)
|
||||
if len(files) > 0 && Button(c, msg(c, "recent_files.clear"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() && !disabled {
|
||||
res.Cleared = true
|
||||
slog.Debug("mujica: recent_files clear")
|
||||
}
|
||||
})
|
||||
if len(files) == 0 {
|
||||
codeCaption(c, msg(c, "recent_files.empty")).Padding(d.Unit() * 4)
|
||||
return
|
||||
}
|
||||
s.rows.Selected = &s.selected
|
||||
s.rows.Key = func(i int) any {
|
||||
r := s.flat[i]
|
||||
if r.file < 0 {
|
||||
return r.group
|
||||
}
|
||||
return files[r.file].Path
|
||||
}
|
||||
s.rows.Header = func(i int) bool { return s.flat[i].file < 0 }
|
||||
s.rows.Label = func(i int) string {
|
||||
if r := s.flat[i]; r.file >= 0 {
|
||||
return path.Base(files[r.file].Path)
|
||||
}
|
||||
return recentGroupName(c, s.flat[i].group)
|
||||
}
|
||||
list := codeRows(c, &s.rows, len(s.flat), func(i int) {
|
||||
r := s.flat[i]
|
||||
if r.file < 0 {
|
||||
ui.Text(c, recentGroupName(c, r.group)).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).
|
||||
TextColor(k.Ornament.Mix(k.Text, 0.2)).Grow(1)
|
||||
return
|
||||
}
|
||||
f := files[r.file]
|
||||
FileIcon(c, f.Path, FileIconOptions{})
|
||||
ui.Column(c).Grow(1).MinWidth(0).Children(func() {
|
||||
ui.Text(c, path.Base(f.Path)).SingleLine()
|
||||
codeCaption(c, path.Dir(f.Path))
|
||||
})
|
||||
when := f.Opened.Format("15:04")
|
||||
if r.group >= 2 {
|
||||
when = f.Opened.Format("Jan 2")
|
||||
}
|
||||
codeCaption(c, when).Shrink(0)
|
||||
if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "recent_files.remove"), path.Base(f.Path))).Clicked() && !disabled {
|
||||
res.Removed = f.Path
|
||||
slog.Debug("mujica: recent_files remove")
|
||||
}
|
||||
})
|
||||
list.Background(k.Surface)
|
||||
if list.Submitted() && !disabled && s.selected >= 0 && s.selected < len(s.flat) && s.flat[s.selected].file >= 0 {
|
||||
f := files[s.flat[s.selected].file]
|
||||
res.Opened = &f
|
||||
slog.Debug("mujica: recent_files open")
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func recentGroupName(c *ui.Context, g int) string {
|
||||
return msg(c, [...]string{"recent_files.today", "recent_files.yesterday", "recent_files.week", "recent_files.earlier"}[g])
|
||||
}
|
||||
|
||||
// recentGroup caches files sorted into relative-day groups.
|
||||
func recentGroup(files []RecentFile, s *RecentFilesState, now time.Time) {
|
||||
var first *RecentFile
|
||||
if len(files) > 0 {
|
||||
first = &files[0]
|
||||
}
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
if s.data == first && s.n == len(files) && s.day.Equal(today) {
|
||||
return
|
||||
}
|
||||
s.data, s.n, s.day = first, len(files), today
|
||||
idx := make([]int, len(files))
|
||||
for i := range idx {
|
||||
idx[i] = i
|
||||
}
|
||||
slices.SortStableFunc(idx, func(a, b int) int { return cmp.Compare(files[b].Opened.UnixNano(), files[a].Opened.UnixNano()) })
|
||||
group := func(t time.Time) int {
|
||||
switch {
|
||||
case !t.Before(today):
|
||||
return 0
|
||||
case !t.Before(today.AddDate(0, 0, -1)):
|
||||
return 1
|
||||
case !t.Before(today.AddDate(0, 0, -6)):
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
}
|
||||
s.flat = s.flat[:0]
|
||||
last := -1
|
||||
for _, i := range idx {
|
||||
g := group(files[i].Opened)
|
||||
if g != last {
|
||||
s.flat = append(s.flat, recentRow{g, -1})
|
||||
last = g
|
||||
}
|
||||
s.flat = append(s.flat, recentRow{g, i})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user