WIP components (as built)
This commit is contained in:
102 files changed
+6538
-1
No files matched your search
@@ -0,0 +1,212 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// PomodoroPhase is the part of the cycle running.
|
||||
type PomodoroPhase int
|
||||
|
||||
const (
|
||||
PomodoroFocus PomodoroPhase = iota
|
||||
PomodoroBreak
|
||||
PomodoroLongBreak
|
||||
)
|
||||
|
||||
// PomodoroState holds the cycle, sessions and running clock.
|
||||
type PomodoroState struct {
|
||||
phase PomodoroPhase
|
||||
focused int
|
||||
running bool
|
||||
since time.Time
|
||||
spent time.Duration
|
||||
}
|
||||
|
||||
// Phase returns the current phase.
|
||||
func (s *PomodoroState) Phase() PomodoroPhase { return s.phase }
|
||||
|
||||
// Sessions returns how many focus sessions finished.
|
||||
func (s *PomodoroState) Sessions() int { return s.focused }
|
||||
|
||||
// Running reports whether the clock runs.
|
||||
func (s *PomodoroState) Running() bool { return s.running }
|
||||
|
||||
func (s *PomodoroState) elapsed(now time.Time) time.Duration {
|
||||
if s.running {
|
||||
return s.spent + now.Sub(s.since)
|
||||
}
|
||||
return s.spent
|
||||
}
|
||||
|
||||
// PomodoroTimerOptions configure a PomodoroTimer.
|
||||
type PomodoroTimerOptions struct {
|
||||
// Focus/Break/LongBreak default to 25/5/15 minutes.
|
||||
Focus, Break, LongBreak time.Duration
|
||||
// LongEvery counts sessions per long break; zero means four.
|
||||
LongEvery int
|
||||
// Now fixes the clock; zero uses the system clock.
|
||||
Now time.Time
|
||||
Disabled bool
|
||||
Label string
|
||||
}
|
||||
|
||||
// PomodoroResult is the timer and what changed this frame.
|
||||
type PomodoroResult struct {
|
||||
Element *ui.Element
|
||||
changed bool
|
||||
finished PomodoroPhase
|
||||
done bool
|
||||
}
|
||||
|
||||
// Changed reports a start, pause, skip or reset.
|
||||
func (r PomodoroResult) Changed() bool { return r.changed }
|
||||
|
||||
// Finished returns the phase that ran out this frame.
|
||||
func (r PomodoroResult) Finished() (PomodoroPhase, bool) { return r.finished, r.done }
|
||||
|
||||
// PomodoroTimer cycles focus and breaks with pause, skip and reset.
|
||||
func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) PomodoroResult {
|
||||
lengths := [3]time.Duration{opts.Focus, opts.Break, opts.LongBreak}
|
||||
for i, def := range []time.Duration{25 * time.Minute, 5 * time.Minute, 15 * time.Minute} {
|
||||
if lengths[i] < 0 || lengths[i]%time.Second != 0 {
|
||||
panic(fmt.Sprintf("mujica: PomodoroTimer length %v is negative or finer than a second", lengths[i]))
|
||||
}
|
||||
if lengths[i] == 0 {
|
||||
lengths[i] = def
|
||||
}
|
||||
}
|
||||
every := opts.LongEvery
|
||||
if every < 0 {
|
||||
panic("mujica: PomodoroTimer LongEvery is negative")
|
||||
}
|
||||
if every == 0 {
|
||||
every = 4
|
||||
}
|
||||
now := opts.Now
|
||||
live := now.IsZero()
|
||||
if live {
|
||||
now = c.Now()
|
||||
}
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = msg(c, "pomodoro_timer.label")
|
||||
}
|
||||
r := PomodoroResult{}
|
||||
next := func() {
|
||||
if s.phase == PomodoroFocus {
|
||||
s.focused++
|
||||
s.phase = PomodoroBreak
|
||||
if s.focused%every == 0 {
|
||||
s.phase = PomodoroLongBreak
|
||||
}
|
||||
} else {
|
||||
s.phase = PomodoroFocus
|
||||
}
|
||||
s.running, s.spent = false, 0
|
||||
}
|
||||
if s.running && s.elapsed(now) >= lengths[s.phase] {
|
||||
r.finished, r.done = s.phase, true
|
||||
c.Announce(fmt.Sprintf(msg(c, "pomodoro_timer.done"), msg(c, pomodoroKeys[s.phase])))
|
||||
slog.Debug("mujica: pomodoro_timer finished", "phase", s.phase)
|
||||
next()
|
||||
}
|
||||
col := ui.Column(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
||||
r.Element = col
|
||||
act := func(what string, fn func()) {
|
||||
if col.IsDisabled() {
|
||||
return
|
||||
}
|
||||
fn()
|
||||
r.changed = true
|
||||
slog.Debug("mujica: pomodoro_timer "+what, "phase", s.phase)
|
||||
}
|
||||
left := lengths[s.phase] - s.elapsed(now)
|
||||
secs := int(math.Ceil(left.Seconds()))
|
||||
frac := 1 - float32(secs)/float32(lengths[s.phase]/time.Second)
|
||||
col.Children(func() {
|
||||
phase := msg(c, pomodoroKeys[s.phase])
|
||||
text := fmt.Sprintf("%02d:%02d", secs/60, secs%60)
|
||||
ring := ui.Box(c).Size(160, 160).Center().Role(ui.RoleStatus).Label(fmt.Sprintf(msg(c, "pomodoro_timer.left"), phase, CountdownText(c, time.Duration(secs)*time.Second, CountdownWords)))
|
||||
tone := k.AccentText
|
||||
if s.phase != PomodoroFocus {
|
||||
tone = k.Success
|
||||
}
|
||||
ring.Draw(func(p *ui.Painter, rr ui.Rect) {
|
||||
cx, cy := internal.Snap(p, rr.X+rr.W/2), internal.Snap(p, rr.Y+rr.H/2)
|
||||
p.FillPath(pomodoroSector(p, cx, cy, 70, 76, 2*math.Pi), k.Border)
|
||||
if frac > 0 && !c.Preferences().ReduceMotion {
|
||||
p.FillPath(pomodoroSector(p, cx, cy, 70, 76, 2*math.Pi*float64(frac)), tone)
|
||||
}
|
||||
})
|
||||
ring.Children(func() {
|
||||
ui.Column(c).AlignItems(ui.Center).Gap(2).Children(func() {
|
||||
ui.Text(c, text).Font(theme.MonoFont).FontSize(FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum")
|
||||
ui.Text(c, phase).FontSize(FontSize(c, theme.CaptionSize)).TextColor(tone)
|
||||
})
|
||||
})
|
||||
ui.Text(c, fmt.Sprintf(msg(c, "pomodoro_timer.sessions"), s.focused%every, every)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
||||
ui.Row(c).Gap(d.Unit() * 2).Children(func() {
|
||||
key, icon, variant := "pomodoro_timer.start", "play", Primary
|
||||
if s.running {
|
||||
key, icon, variant = "pomodoro_timer.pause", "pause", Secondary
|
||||
}
|
||||
if Button(c, msg(c, key), ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() {
|
||||
act("toggle", func() {
|
||||
if s.running {
|
||||
s.spent, s.running = s.elapsed(now), false
|
||||
} else {
|
||||
s.since, s.running = now, true
|
||||
}
|
||||
})
|
||||
}
|
||||
if Button(c, msg(c, "pomodoro_timer.skip"), ButtonOptions{Variant: Secondary}).Clicked() {
|
||||
act("skip", next)
|
||||
}
|
||||
if Button(c, msg(c, "pomodoro_timer.reset"), ButtonOptions{Variant: Ghost, Icon: icons.Must("rotate-ccw")}).Clicked() {
|
||||
act("reset", func() { *s = PomodoroState{} })
|
||||
}
|
||||
})
|
||||
})
|
||||
if s.running && live {
|
||||
delay := left % time.Second
|
||||
if delay == 0 {
|
||||
delay = time.Second
|
||||
}
|
||||
c.After(delay)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var pomodoroKeys = [...]string{"pomodoro_timer.focus", "pomodoro_timer.break", "pomodoro_timer.long_break"}
|
||||
|
||||
// pomodoroSector snaps every changing vertex to the cache grid.
|
||||
func pomodoroSector(p *ui.Painter, cx, cy, inner, outer float32, end float64) *ui.Path {
|
||||
var path ui.Path
|
||||
n := max(2, int(math.Ceil(end/(math.Pi/48))))
|
||||
for i := 0; i <= n; i++ {
|
||||
x, y := internal.Polar(cx, cy, outer, end*float64(i)/float64(n))
|
||||
x, y = internal.Snap(p, x), internal.Snap(p, y)
|
||||
if i == 0 {
|
||||
path.MoveTo(x, y)
|
||||
} else {
|
||||
path.LineTo(x, y)
|
||||
}
|
||||
}
|
||||
for i := n; i >= 0; i-- {
|
||||
x, y := internal.Polar(cx, cy, inner, end*float64(i)/float64(n))
|
||||
path.LineTo(internal.Snap(p, x), internal.Snap(p, y))
|
||||
}
|
||||
path.Close()
|
||||
return &path
|
||||
}
|
||||
Reference in new issue
Block a user