Works now, also reorganised code

This commit is contained in:
Melody Becker 2025-02-03 16:41:15 +01:00
parent 185c84742a
commit 781f6a10ec
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
12 changed files with 316 additions and 170 deletions

View file

@ -1,7 +0,0 @@
package main
import "image/color"
var (
ColorRed = color.NRGBA{R: 0xff, G: 0x0, B: 0x0, A: 255}
)

2
go.mod
View file

@ -5,7 +5,7 @@ go 1.23.5
require (
gioui.org v0.8.0 // indirect
gioui.org/shader v1.0.8 // indirect
git.mstar.dev/mstar/goutils v1.6.1 // indirect
git.mstar.dev/mstar/goutils v1.6.2 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect

2
go.sum
View file

@ -5,6 +5,8 @@ gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
git.mstar.dev/mstar/goutils v1.6.1 h1:2yr9GYN8CJByZsJRu1pZ6WBp51Nn+3zJq49ky54xYDk=
git.mstar.dev/mstar/goutils v1.6.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA=
git.mstar.dev/mstar/goutils v1.6.2 h1:yqpEXQJWWiNZaJ2aenG5iCtjNP/gEnX9h5Ui4hrQ1tw=
git.mstar.dev/mstar/goutils v1.6.2/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=

42
main.go
View file

@ -2,32 +2,40 @@ package main
import (
"os"
"time"
"sync"
"gioui.org/app"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/timer/shared"
"git.mstar.dev/mstar/timer/timer"
"github.com/rs/zerolog/log"
)
var t time.Time
func main() {
window := new(app.Window)
var err error
t, err = time.Parse(time.RFC3339, "2025-01-31T22:50:00Z")
if err != nil {
log.Fatal().Err(err).Send()
other.SetupFlags()
other.ConfigureLoggingFromCliArgs()
// Create window struct outside. Doesn't cause any renders yet
windows := []*app.Window{
new(app.Window),
}
var wg sync.WaitGroup
for _, window := range windows {
wg.Add(1)
go windowLauncher(window, &wg)
}
go func() {
// window.Option(func(m unit.Metric, c *app.Config) {
// c.Size = image.Pt(500, 150)
// c.MaxSize = image.Pt(500, 150)
// c.MinSize = image.Pt(500, 150)
// })
err := run(window, &StateEnterTime{})
if err != nil {
log.Fatal().Err(err).Msg("Failed to run main window")
}
wg.Wait()
log.Info().Msg("All windows closed, exiting")
os.Exit(0)
}()
app.Main()
}
func windowLauncher(window *app.Window, wg *sync.WaitGroup) {
defer wg.Done()
log.Info().Msg("Starting window")
err := shared.Run(window, &timer.StateEnterTime{})
if err != nil {
log.Fatal().Err(err).Msg("Failed to run main window")
}
}

14
shared/constants.go Normal file
View file

@ -0,0 +1,14 @@
package shared
import (
"errors"
"image/color"
)
var (
ColorRed = color.NRGBA{R: 0xff, G: 0x0, B: 0x0, A: 255}
)
var (
ErrExitOk = errors.New("exit ok")
)

6
shared/theming.go Normal file
View file

@ -0,0 +1,6 @@
package shared
import "image/color"
var BackgroundColor = color.NRGBA{R: 0x0, G: 0x0, B: 0x0, A: 0xff}
var TextColor = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}

46
shared/window.go Normal file
View file

@ -0,0 +1,46 @@
package shared
import (
"sync"
"gioui.org/app"
"gioui.org/widget/material"
"github.com/rs/zerolog/log"
)
type GlobalState struct {
Window *app.Window
Theme *material.Theme
state WindowState
}
func Run(window *app.Window, initialState WindowState) error {
// Set up global data first
state := GlobalState{}
theme := material.NewTheme()
theme.Bg = BackgroundColor
theme.Fg = TextColor
state.Theme = theme
state.Window = window
state.state = initialState
// Then start showing the window and polling for events
// Though technically, I don't think window.Event will cause the window to display yet
// since what should it display? It doesn't have anything to display. So first display happens at event.Frame
for {
e := window.Event()
// Let the current view handle the event, then update accordingly
newState := state.state.Run(&state, e)
if newState.NextState != nil {
state.state = newState.NextState
}
if newState.ExitCode != nil {
log.Info().Msg("Received shutdown request")
wg := sync.WaitGroup{}
wg.Add(1)
go state.state.Exit(&state, &wg)
wg.Wait()
log.Info().Msg("Shutdown of window complete")
return nil
}
}
}

View file

@ -1,18 +1,21 @@
package main
package shared
import (
"sync"
"gioui.org/io/event"
)
type WindowState interface {
Run(globalState *GlobalState, event event.Event) NewState
Exit(globalState *GlobalState, wg *sync.WaitGroup)
}
type NewState struct {
// If null, keep current state
NextState WindowState
Error error
ExitCode *int
ExitCode error
}
func EmptyEvent() NewState {

88
timer/windowCountdown.go Normal file
View file

@ -0,0 +1,88 @@
package timer
import (
"image"
"sync"
"time"
"gioui.org/app"
"gioui.org/io/event"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/widget/material"
"git.mstar.dev/mstar/timer/shared"
"github.com/rs/zerolog/log"
)
type StateCountdown struct {
initialSetupDone bool
TargetTime time.Time
ops op.Ops
}
func (s *StateCountdown) Run(globalState *shared.GlobalState, event event.Event) shared.NewState {
if !s.initialSetupDone {
s.initialSetup(globalState)
}
switch event := event.(type) {
case app.FrameEvent:
return s.handleFrameEvent(globalState, &event)
case app.DestroyEvent:
return s.handleQuitEvent()
case app.WaylandViewEvent, app.X11ViewEvent:
return shared.EmptyEvent()
default:
log.Debug().Any("event", event).Type("type", event).Msg("Unknown event")
return shared.NewState{}
}
}
func (s *StateCountdown) Exit(globalState *shared.GlobalState, wg *sync.WaitGroup) {
wg.Done()
}
func (s *StateCountdown) initialSetup(globalState *shared.GlobalState) {
s.initialSetupDone = true
go func(window *app.Window) {
t := time.NewTicker(time.Second)
for range t.C {
window.Invalidate()
}
}(globalState.Window)
}
func (s *StateCountdown) handleQuitEvent() shared.NewState {
return shared.NewState{ExitCode: shared.ErrExitOk}
}
func (s *StateCountdown) handleFrameEvent(
globalState *shared.GlobalState,
frameEvent *app.FrameEvent,
) shared.NewState {
gtx := app.NewContext(&s.ops, *frameEvent)
paint.FillShape(
gtx.Ops,
globalState.Theme.Bg,
clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op(),
)
diff := frameEvent.Now.Sub(s.TargetTime)
diff *= -1
title := material.H1(
globalState.Theme,
diff.Round(time.Second).String(),
)
// //
// // // Change the color of the label.
title.Color = shared.TextColor
// //
// // // Change the position of the label.
title.Alignment = text.Middle
//
// // Draw the label to the graphics context.
title.Layout(gtx)
// // Pass the drawing operations to the GPU.
frameEvent.Frame(&s.ops)
return shared.NewState{}
}

129
timer/windowEnterTime.go Normal file
View file

@ -0,0 +1,129 @@
package timer
import (
"image"
"sync"
"time"
"gioui.org/app"
"gioui.org/io/event"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/timer/shared"
"github.com/rs/zerolog/log"
)
type StateEnterTime struct {
initialSetupDone bool
editor *widget.Editor
inputField *material.EditorStyle
inputErrorMessage *string
ops op.Ops
}
func (s *StateEnterTime) initialSetup(globalState *shared.GlobalState) {
s.editor = &widget.Editor{
SingleLine: true,
Submit: true,
Filter: "01234567890TZ+-:",
}
field := material.Editor(globalState.Theme, s.editor, "timestamp")
s.inputField = &field
s.initialSetupDone = true
globalState.Window.Option(func(m unit.Metric, c *app.Config) {
c.MaxSize.X = 520
c.MaxSize.Y = 150
c.MinSize = c.MaxSize
c.Size = c.MaxSize
})
}
func (s *StateEnterTime) Run(globalState *shared.GlobalState, event event.Event) shared.NewState {
if !s.initialSetupDone {
s.initialSetup(globalState)
}
switch event := event.(type) {
case app.FrameEvent:
return s.handleFrameEvent(globalState, &event)
case app.DestroyEvent:
return s.handleQuitEvent()
case app.WaylandViewEvent, app.X11ViewEvent:
return shared.EmptyEvent()
default:
log.Debug().Any("event", event).Type("type", event).Msg("Unknown event")
return shared.NewState{}
}
}
func (s *StateEnterTime) Exit(globalState *shared.GlobalState, wg *sync.WaitGroup) {
wg.Done()
}
func (s *StateEnterTime) handleFrameEvent(
globalState *shared.GlobalState,
frameEvent *app.FrameEvent,
) shared.NewState {
gtx := app.NewContext(&s.ops, *frameEvent)
for ev, ok := s.editor.Update(gtx); ok; ev, ok = s.editor.Update(gtx) {
switch ev := ev.(type) {
case widget.SubmitEvent:
t, err := time.Parse(time.RFC3339, ev.Text)
if err == nil {
return s.handleTimeEntered(t)
}
s.inputErrorMessage = other.IntoPointer(err.Error())
log.Debug().Err(err).Msg("Invalid timestamp")
}
log.Debug().Any("Editor event", ev).Type("type", ev).Send()
}
paint.FillShape(
gtx.Ops,
globalState.Theme.Bg,
clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op(),
)
label := material.H3(globalState.Theme, "Enter timestamp")
hint := material.Subtitle1(
globalState.Theme,
"Format: <YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>(Z|+<TZ>)",
)
var errorMessage material.LabelStyle
nrOfElements := 3
if s.inputErrorMessage != nil {
nrOfElements++
errorMessage = material.Subtitle2(globalState.Theme, *s.inputErrorMessage)
}
list := layout.List{Axis: layout.Vertical}
list.Layout(gtx, nrOfElements, func(gtx layout.Context, index int) layout.Dimensions {
switch index {
case 0:
return label.Layout(gtx)
case 1:
return hint.Layout(gtx)
case 2:
return s.inputField.Layout(gtx)
case 3:
return errorMessage.Layout(gtx)
default:
return hint.Layout(gtx)
}
})
frameEvent.Frame(&s.ops)
return shared.NewState{}
}
func (s *StateEnterTime) handleQuitEvent() shared.NewState {
return shared.NewState{ExitCode: shared.ErrExitOk}
}
func (s *StateEnterTime) handleTimeEntered(stamp time.Time) shared.NewState {
// TODO: Switch to countdown state once that is implemented
return shared.NewState{
NextState: &StateCountdown{TargetTime: stamp},
}
}

View file

@ -1,62 +0,0 @@
package main
import (
"image/color"
"os"
"time"
"gioui.org/app"
"gioui.org/widget/material"
)
type GlobalState struct {
Window *app.Window
Theme *material.Theme
state WindowState
}
func ticker(window *app.Window) {
t := time.NewTicker(time.Second)
for range t.C {
window.Invalidate()
}
}
func run(window *app.Window, initialState WindowState) error {
state := GlobalState{}
theme := material.NewTheme()
theme.Bg = color.NRGBA{A: 0xff}
state.Theme = theme
state.Window = window
state.state = initialState
for {
e := window.Event()
newState := state.state.Run(&state, e)
if newState.NextState != nil {
state.state = newState.NextState
}
if newState.ExitCode != nil {
os.Exit(*newState.ExitCode)
}
// log.Debug().Any("size", e.Size).Msg("Redrawing")
// // This graphics context is used for managing the rendering state.
// gtx := app.NewContext(&ops, e)
// paint.FillShape(gtx.Ops, theme.Bg, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
// //
// // // Define an large label with an appropriate text:
// log.Debug().Dur("d", t.Sub(e.Now)).Time("t", t).Send()
// title := material.H1(theme, e.Now.Sub(t).Round(time.Second).String())
// //
// // // Change the color of the label.
// maroon := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 255}
// title.Color = maroon
// //
// // // Change the position of the label.
// title.Alignment = text.Middle
//
// // Draw the label to the graphics context.
// title.Layout(gtx)
// // Pass the drawing operations to the GPU.
// e.Frame(&ops)
}
}

View file

@ -1,81 +0,0 @@
package main
import (
"gioui.org/app"
"gioui.org/io/event"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
"gioui.org/widget/material"
"git.mstar.dev/mstar/goutils/other"
"github.com/rs/zerolog/log"
)
type StateEnterTime struct {
initialSetupDone bool
editor *widget.Editor
inputField *material.EditorStyle
ops op.Ops
}
func (s *StateEnterTime) initialSetup(globalState *GlobalState) {
s.editor = &widget.Editor{
SingleLine: true,
Submit: true,
Filter: "01234567890TZ+-:",
}
field := material.Editor(globalState.Theme, s.editor, "timestamp")
s.inputField = &field
s.initialSetupDone = true
}
func (s *StateEnterTime) Run(globalState *GlobalState, event event.Event) NewState {
if !s.initialSetupDone {
s.initialSetup(globalState)
}
switch event := event.(type) {
case app.FrameEvent:
return s.handleFrameEvent(globalState, &event)
case app.DestroyEvent:
return s.handleQuitEvent()
case app.WaylandViewEvent, app.X11ViewEvent:
return EmptyEvent()
default:
log.Debug().Any("event", event).Type("type", event).Msg("Unknown event")
return NewState{}
}
}
func (s *StateEnterTime) handleFrameEvent(
globalState *GlobalState,
frameEvent *app.FrameEvent,
) NewState {
gtx := app.NewContext(&s.ops, *frameEvent)
for ev, ok := s.editor.Update(gtx); ok; ev, ok = s.editor.Update(gtx) {
log.Debug().Any("Editor event", ev).Send()
}
label := material.H3(globalState.Theme, "Enter timestamp")
hint := material.Subtitle1(
globalState.Theme,
"Format: <YYYY>-<MM>-<DD>T<hh>:<mm>:<ss>(Z|+<TZ>)",
)
list := layout.List{Axis: layout.Vertical}
list.Layout(gtx, 3, func(gtx layout.Context, index int) layout.Dimensions {
switch index {
case 0:
return label.Layout(gtx)
case 1:
return hint.Layout(gtx)
case 2:
return s.inputField.Layout(gtx)
default:
return hint.Layout(gtx)
}
})
frameEvent.Frame(&s.ops)
return NewState{}
}
func (s *StateEnterTime) handleQuitEvent() NewState {
return NewState{ExitCode: other.IntoPointer(0)}
}