Works now, also reorganised code
This commit is contained in:
parent
185c84742a
commit
781f6a10ec
12 changed files with 316 additions and 170 deletions
|
@ -1,7 +0,0 @@
|
|||
package main
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
ColorRed = color.NRGBA{R: 0xff, G: 0x0, B: 0x0, A: 255}
|
||||
)
|
2
go.mod
2
go.mod
|
@ -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
2
go.sum
|
@ -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
42
main.go
|
@ -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
14
shared/constants.go
Normal 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
6
shared/theming.go
Normal 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
46
shared/window.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
88
timer/windowCountdown.go
Normal 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
129
timer/windowEnterTime.go
Normal 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},
|
||||
}
|
||||
}
|
62
window.go
62
window.go
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)}
|
||||
}
|
Loading…
Reference in a new issue