diff --git a/README.md b/README.md index 8fa0973..c9354db 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ most complex. * Works the same as CheckButton and RadioButton but draws a separate label next to a small check button. Clicking the label will toggle the state of the checkbox. +* [x] **Pager**: a series of numbered buttons to use with a paginated UI. + Includes "Forward" and "Next" buttons and buttons for each page number. * [x] **Window**: a Frame with a title bar Frame on top. * Can be managed by Supervisor to give Window Manager controls to it (drag it by its title bar, Close button, window focus, multiple overlapping diff --git a/pager.go b/pager.go new file mode 100644 index 0000000..bee9db1 --- /dev/null +++ b/pager.go @@ -0,0 +1,213 @@ +package ui + +import ( + "fmt" + "strconv" + + "git.kirsle.net/go/render" +) + +// Pager is a frame with Pagers for paginated UI. +type Pager struct { + BaseWidget + + // Config settings. + Page int // default 1 + Pages int + PerPage int // default 20 + Font render.Text + OnChange func(page, perPage int) + + supervisor *Supervisor + child Widget + buttons []Widget + page string // radio button value of Page + + // Private options. + hovering bool + clicked bool +} + +// NewPager creates a new Pager. +func NewPager(config Pager) *Pager { + w := &Pager{ + Page: config.Page, + Pages: config.Pages, + PerPage: config.PerPage, + Font: config.Font, + OnChange: config.OnChange, + buttons: []Widget{}, + } + + // default settings + if w.Page == 0 { + w.Page = 1 + } + if w.PerPage == 0 { + w.PerPage = 20 + } + + w.IDFunc(func() string { + return fmt.Sprintf("Pager<%d of %d>", w.Page, w.PerPage) + }) + + w.child = w.setup() + + return w +} + +// Supervise the pager to make its buttons work. +func (w *Pager) Supervise(s *Supervisor) { + w.supervisor = s + for _, btn := range w.buttons { + w.supervisor.Add(btn) + } +} + +// setup the frame +func (w *Pager) setup() *Frame { + frame := NewFrame("Pager Frame") + frame.SetParent(w) + + if w.Pages == 0 { + return frame + } + + w.buttons = []Widget{} + w.page = fmt.Sprintf("%d", w.Page) + + // Previous Page Button + prev := NewButton("Previous", NewLabel(Label{ + Text: "<", + Font: w.Font, + })) + w.buttons = append(w.buttons, prev) + prev.Handle(Click, func(ed EventData) error { + return w.next(-1) + }) + frame.Pack(prev, Pack{ + Side: W, + }) + + // Draw the numbered buttons. + for i := 1; i <= w.Pages; i++ { + page := fmt.Sprintf("%d", i) + + btn := NewRadioButton( + "Page "+page, + &w.page, + page, + NewLabel(Label{ + Text: page, + Font: w.Font, + })) + w.buttons = append(w.buttons, btn) + + btn.Handle(Click, func(ed EventData) error { + if w.OnChange != nil { + page, _ := strconv.Atoi(w.page) + w.OnChange(page, w.PerPage) + } + return nil + }) + + if w.supervisor != nil { + w.supervisor.Add(btn) + } + + frame.Pack(btn, Pack{ + Side: W, + }) + } + + // Next Page Button + next := NewButton("Next", NewLabel(Label{ + Text: ">", + Font: w.Font, + })) + w.buttons = append(w.buttons, next) + next.Handle(Click, func(ed EventData) error { + return w.next(1) + }) + frame.Pack(next, Pack{ + Side: W, + }) + + return frame +} + +// next (1) or previous (-1) button +func (w *Pager) next(value int) error { + fmt.Printf("next(%d)\n", value) + intvalue, _ := strconv.Atoi(w.page) + intvalue += value + + if intvalue < 1 { + intvalue = 1 + } else if intvalue > w.Pages { + intvalue = w.Pages + } + + w.page = fmt.Sprintf("%d", intvalue) + + if w.OnChange != nil { + w.OnChange(intvalue, w.PerPage) + } + + return nil +} + +// Compute the size of the Pager. +func (w *Pager) Compute(e render.Engine) { + // Compute the size of the inner widget first. + w.child.Compute(e) + + // Auto-resize only if we haven't been given a fixed size. + if !w.FixedSize() { + size := w.child.Size() + w.Resize(render.Rect{ + W: size.W + w.BoxThickness(2), + H: size.H + w.BoxThickness(2), + }) + } + + w.BaseWidget.Compute(e) +} + +// Present the Pager. +func (w *Pager) Present(e render.Engine, P render.Point) { + if w.Hidden() { + return + } + + w.Compute(e) + var ( + S = w.Size() + ChildSize = w.child.Size() + ) + + // Draw the widget's border and everything. + w.DrawBox(e, P) + + // Offset further if we are currently sunken. + var clickOffset int + if w.clicked { + clickOffset++ + } + + // Where to place the child widget. + moveTo := render.Point{ + X: P.X + w.BoxThickness(1) + clickOffset, + Y: P.Y + w.BoxThickness(1) + clickOffset, + } + + // If we're bigger than we need to be, center the child widget. + if S.Bigger(ChildSize) { + moveTo.X = P.X + (S.W / 2) - (ChildSize.W / 2) + } + + // Draw the text label inside. + w.child.Present(e, moveTo) + + w.BaseWidget.Present(e, P) +}