menu.go (5906B)
1 package render 2 3 import ( 4 "context" 5 "fmt" 6 7 "git.defalsify.org/vise.git/resource" 8 ) 9 10 // BrowseError is raised when browsing outside the page range of a rendered node. 11 type BrowseError struct { 12 Idx uint16 13 PageCount uint16 14 } 15 16 // Error implements the Error interface. 17 func(err *BrowseError) Error() string { 18 return fmt.Sprintf("index is out of bounds: %v", err.Idx) 19 } 20 21 // BrowseConfig defines the availability and display parameters for page browsing. 22 type BrowseConfig struct { 23 NextAvailable bool 24 NextSelector string 25 NextTitle string 26 PreviousAvailable bool 27 PreviousSelector string 28 PreviousTitle string 29 } 30 31 // Default browse settings for convenience. 32 func DefaultBrowseConfig() BrowseConfig { 33 return BrowseConfig{ 34 NextAvailable: true, 35 NextSelector: "11", 36 NextTitle: "next", 37 PreviousAvailable: true, 38 PreviousSelector: "22", 39 PreviousTitle: "previous", 40 } 41 } 42 43 // Menu renders menus. May be included in a Page object to render menus for pages. 44 type Menu struct { 45 rs resource.Resource 46 menu [][2]string // selector and title for menu items. 47 browse BrowseConfig // browse definitions. 48 pageCount uint16 // number of pages the menu should represent. 49 canNext bool // availability flag for the "next" browse option. 50 canPrevious bool // availability flag for the "previous" browse option. 51 //outputSize uint16 // maximum size constraint for the menu. 52 sink bool 53 keep bool 54 } 55 56 func(m Menu) String() string { 57 return fmt.Sprintf("pagecount: %v menusink: %v next: %v prev: %v", m.pageCount, m.sink, m.canNext, m.canPrevious) 58 } 59 60 // NewMenu creates a new Menu with an explicit page count. 61 func NewMenu() *Menu { 62 return &Menu{ 63 keep: true, 64 } 65 } 66 67 // WithBrowseConfig defines the criteria for page browsing. 68 func(m *Menu) WithPageCount(pageCount uint16) *Menu { 69 m.pageCount = pageCount 70 return m 71 } 72 73 func(m *Menu) WithPages() *Menu { 74 if m.pageCount == 0 { 75 m.pageCount = 1 76 } 77 return m 78 } 79 80 func(m *Menu) WithSink() *Menu { 81 m.sink = true 82 return m 83 } 84 85 func(m *Menu) WithDispose() *Menu { 86 m.keep = false 87 return m 88 } 89 90 func(m *Menu) WithResource(rs resource.Resource) *Menu { 91 m.rs = rs 92 return m 93 } 94 95 func(m Menu) IsSink() bool { 96 return m.sink 97 } 98 99 // WithSize defines the maximum byte size of the rendered menu. 100 //func(m *Menu) WithOutputSize(outputSize uint16) *Menu { 101 // m.outputSize = outputSize 102 // return m 103 //} 104 105 // GetOutputSize returns the defined heuristic menu size. 106 //func(m *Menu) GetOutputSize() uint32 { 107 // return uint32(m.outputSize) 108 //} 109 110 // WithBrowseConfig defines the criteria for page browsing. 111 func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { 112 m.browse = cfg 113 return m 114 } 115 116 // GetBrowseConfig returns a copy of the current state of the browse configuration. 117 func(m *Menu) GetBrowseConfig() BrowseConfig { 118 return m.browse 119 } 120 121 // Put adds a menu option to the menu rendering. 122 func(m *Menu) Put(selector string, title string) error { 123 m.menu = append(m.menu, [2]string{selector, title}) 124 return nil 125 } 126 127 // ReservedSize returns the maximum render byte size of the menu. 128 //func(m *Menu) ReservedSize() uint16 { 129 // return m.outputSize 130 //} 131 132 // mainSize, prevsize, nextsize, nextsize+prevsize 133 func(m *Menu) Sizes(ctx context.Context) ([4]uint32, error) { 134 var menuSizes [4]uint32 135 cfg := m.GetBrowseConfig() 136 tmpm := NewMenu().WithBrowseConfig(cfg) 137 v, err := tmpm.Render(ctx, 0) 138 if err != nil { 139 return menuSizes, err 140 } 141 menuSizes[0] = uint32(len(v)) 142 tmpm = tmpm.WithPageCount(2) 143 v, err = tmpm.Render(ctx, 0) 144 if err != nil { 145 return menuSizes, err 146 } 147 menuSizes[1] = uint32(len(v)) - menuSizes[0] 148 v, err = tmpm.Render(ctx, 1) 149 if err != nil { 150 return menuSizes, err 151 } 152 menuSizes[2] = uint32(len(v)) - menuSizes[0] 153 menuSizes[3] = menuSizes[1] + menuSizes[2] 154 return menuSizes, nil 155 } 156 157 func(m *Menu) titleFor(ctx context.Context, title string) (string, error) { 158 if m.rs == nil { 159 return title, nil 160 } 161 r, err := m.rs.GetMenu(ctx, title) 162 if err != nil { 163 return title, err 164 } 165 return r, nil 166 } 167 168 // Render returns the full current state of the menu as a string. 169 // 170 // After this has been executed, the state of the menu will be empty. 171 func(m *Menu) Render(ctx context.Context, idx uint16) (string, error) { 172 var menuCopy [][2]string 173 if m.keep { 174 for _, v := range m.menu { 175 menuCopy = append(menuCopy, v) 176 } 177 } 178 179 err := m.applyPage(idx) 180 if err != nil { 181 return "", err 182 } 183 184 r := "" 185 for true { 186 l := len(r) 187 choice, title, err := m.shiftMenu() 188 if err != nil { 189 break 190 } 191 if l > 0 { 192 r += "\n" 193 } 194 title, err = m.titleFor(ctx, title) 195 if err != nil { 196 return "", err 197 } 198 r += fmt.Sprintf("%s:%s", choice, title) 199 } 200 if m.keep { 201 m.menu = menuCopy 202 } 203 return r, nil 204 } 205 206 // add available browse options. 207 func(m *Menu) applyPage(idx uint16) error { 208 if m.pageCount == 0 { 209 if idx > 0 { 210 return fmt.Errorf("index %v > 0 for non-paged menu", idx) 211 } 212 return nil 213 } else if idx >= m.pageCount { 214 return &BrowseError{Idx: idx, PageCount: m.pageCount} 215 //return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount) 216 } 217 218 m.reset() 219 220 if idx == m.pageCount - 1 { 221 m.canNext = false 222 } 223 if idx == 0 { 224 m.canPrevious = false 225 } 226 Logg.Debugf("applypage", "m", m, "idx", idx) 227 228 if m.canNext { 229 err := m.Put(m.browse.NextSelector, m.browse.NextTitle) 230 if err != nil { 231 return err 232 } 233 } 234 if m.canPrevious { 235 err := m.Put(m.browse.PreviousSelector, m.browse.PreviousTitle) 236 if err != nil { 237 return err 238 } 239 } 240 return nil 241 } 242 243 // removes and returns the first of remaining menu options. 244 // fails if menu is empty. 245 func(m *Menu) shiftMenu() (string, string, error) { 246 if len(m.menu) == 0 { 247 return "", "", fmt.Errorf("menu is empty") 248 } 249 r := m.menu[0] 250 m.menu = m.menu[1:] 251 return r[0], r[1], nil 252 } 253 254 // prepare menu object for re-use. 255 func(m *Menu) reset() { 256 if m.browse.NextAvailable { 257 m.canNext = true 258 } 259 if m.browse.PreviousAvailable { 260 m.canPrevious = true 261 } 262 } 263 264 func(m *Menu) Reset() { 265 m.menu = [][2]string{} 266 m.sink = false 267 m.reset() 268 }