menu.go (7498B)
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 // The lateral page index where the error occurred. 13 Idx uint16 14 // The total number of lateral page indicies. 15 PageCount uint16 16 } 17 18 // Error implements the Error interface. 19 func (err *BrowseError) Error() string { 20 return fmt.Sprintf("index is out of bounds: %v", err.Idx) 21 } 22 23 // BrowseConfig defines the availability and display parameters for page browsing. 24 type BrowseConfig struct { 25 // Set if a consecutive page is available for lateral navigation. 26 NextAvailable bool 27 // Menu selector used to navigate to next page. 28 NextSelector string 29 // Menu title used to label selector for next page. 30 NextTitle string 31 // Set if a previous page is available for lateral navigation. 32 PreviousAvailable bool 33 // Menu selector used to navigate to previous page. 34 PreviousSelector string 35 // Menu title used to label selector for previous page. 36 PreviousTitle string 37 } 38 39 // Create a BrowseConfig with default values. 40 func DefaultBrowseConfig() BrowseConfig { 41 return BrowseConfig{ 42 NextAvailable: true, 43 NextSelector: "11", 44 NextTitle: "next", 45 PreviousAvailable: true, 46 PreviousSelector: "22", 47 PreviousTitle: "previous", 48 } 49 } 50 51 // Menu renders menus. 52 // 53 // May be included in a Page object to render menus for pages. 54 type Menu struct { 55 rs resource.Resource 56 menu [][2]string // selector and title for menu items. 57 browse BrowseConfig // browse definitions. 58 pageCount uint16 // number of pages the menu should represent. 59 canNext bool // availability flag for the "next" browse option. 60 canPrevious bool // availability flag for the "previous" browse option. 61 //outputSize uint16 // maximum size constraint for the menu. 62 sink bool 63 keep bool 64 sep string 65 } 66 67 // String implements the String interface. 68 // 69 // It returns debug representation of menu. 70 func (m Menu) String() string { 71 return fmt.Sprintf("pagecount: %v menusink: %v next: %v prev: %v", m.pageCount, m.sink, m.canNext, m.canPrevious) 72 } 73 74 // NewMenu creates a new Menu with an explicit page count. 75 func NewMenu() *Menu { 76 return &Menu{ 77 keep: true, 78 sep: ":", 79 } 80 } 81 82 // WithPageCount is a chainable function that defines the number of allowed pages for browsing. 83 func (m *Menu) WithSeparator(sep string) *Menu { 84 m.sep = sep 85 return m 86 } 87 88 // WithPageCount is a chainable function that defines the number of allowed pages for browsing. 89 func (m *Menu) WithPageCount(pageCount uint16) *Menu { 90 m.pageCount = pageCount 91 return m 92 } 93 94 // WithPages is a chainable function which activates pagination in the menu. 95 // 96 // It is equivalent to WithPageCount(1) 97 func (m *Menu) WithPages() *Menu { 98 if m.pageCount == 0 { 99 m.pageCount = 1 100 } 101 return m 102 } 103 104 // WithSink is a chainable function that informs the menu that a content sink exists in the render. 105 // 106 // A content sink receives priority to consume all remaining space after all non-sink items have been rendered. 107 func (m *Menu) WithSink() *Menu { 108 m.sink = true 109 return m 110 } 111 112 // WithDispose is a chainable function that preserves the menu after render is complete. 113 // 114 // It is used for multi-page content. 115 func (m *Menu) WithDispose() *Menu { 116 m.keep = false 117 return m 118 } 119 120 func (m *Menu) WithResource(rs resource.Resource) *Menu { 121 m.rs = rs 122 return m 123 } 124 125 func (m Menu) IsSink() bool { 126 return m.sink 127 } 128 129 // WithSize defines the maximum byte size of the rendered menu. 130 //func(m *Menu) WithOutputSize(outputSize uint16) *Menu { 131 // m.outputSize = outputSize 132 // return m 133 //} 134 135 // GetOutputSize returns the defined heuristic menu size. 136 //func(m *Menu) GetOutputSize() uint32 { 137 // return uint32(m.outputSize) 138 //} 139 140 // WithBrowseConfig defines the criteria for page browsing. 141 func (m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { 142 m.browse = cfg 143 return m 144 } 145 146 // GetBrowseConfig returns a copy of the current state of the browse configuration. 147 func (m *Menu) GetBrowseConfig() BrowseConfig { 148 return m.browse 149 } 150 151 // Put adds a menu option to the menu rendering. 152 func (m *Menu) Put(selector string, title string) error { 153 m.menu = append(m.menu, [2]string{selector, title}) 154 return nil 155 } 156 157 // ReservedSize returns the maximum render byte size of the menu. 158 //func(m *Menu) ReservedSize() uint16 { 159 // return m.outputSize 160 //} 161 162 // Sizes returns the size limitations for each part of the render, as a four-element array: 163 // 1. mainSize 164 // 2. prevsize 165 // 3. nextsize 166 // 4. nextsize + prevsize 167 func (m *Menu) Sizes(ctx context.Context) ([4]uint32, error) { 168 var menuSizes [4]uint32 169 cfg := m.GetBrowseConfig() 170 tmpm := NewMenu().WithBrowseConfig(cfg) 171 v, err := tmpm.Render(ctx, 0) 172 if err != nil { 173 return menuSizes, err 174 } 175 menuSizes[0] = uint32(len(v)) 176 tmpm = tmpm.WithPageCount(2) 177 v, err = tmpm.Render(ctx, 0) 178 if err != nil { 179 return menuSizes, err 180 } 181 menuSizes[1] = uint32(len(v)) - menuSizes[0] 182 v, err = tmpm.Render(ctx, 1) 183 if err != nil { 184 return menuSizes, err 185 } 186 menuSizes[2] = uint32(len(v)) - menuSizes[0] 187 menuSizes[3] = menuSizes[1] + menuSizes[2] 188 return menuSizes, nil 189 } 190 191 // title corresponding to the menu symbol. 192 func (m *Menu) titleFor(ctx context.Context, title string) (string, error) { 193 if m.rs == nil { 194 return title, nil 195 } 196 r, err := m.rs.GetMenu(ctx, title) 197 if err != nil { 198 return title, err 199 } 200 return r, nil 201 } 202 203 // Render returns the full current state of the menu as a string. 204 // 205 // After this has been executed, the state of the menu will be empty. 206 func (m *Menu) Render(ctx context.Context, idx uint16) (string, error) { 207 var menuCopy [][2]string 208 if m.keep { 209 for _, v := range m.menu { 210 menuCopy = append(menuCopy, v) 211 } 212 } 213 214 err := m.applyPage(idx) 215 if err != nil { 216 return "", err 217 } 218 219 r := "" 220 for true { 221 l := len(r) 222 choice, title, err := m.shiftMenu() 223 if err != nil { 224 break 225 } 226 if l > 0 { 227 r += "\n" 228 } 229 title, err = m.titleFor(ctx, title) 230 if err != nil { 231 return "", err 232 } 233 r += fmt.Sprintf("%s%s%s", choice, m.sep, title) 234 } 235 if m.keep { 236 m.menu = menuCopy 237 } 238 return r, nil 239 } 240 241 // add available browse options. 242 func (m *Menu) applyPage(idx uint16) error { 243 if m.pageCount == 0 { 244 if idx > 0 { 245 return fmt.Errorf("index %v > 0 for non-paged menu", idx) 246 } 247 return nil 248 } else if idx >= m.pageCount { 249 return &BrowseError{Idx: idx, PageCount: m.pageCount} 250 //return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount) 251 } 252 253 m.reset() 254 255 if idx == m.pageCount-1 { 256 m.canNext = false 257 } 258 if idx == 0 { 259 m.canPrevious = false 260 } 261 logg.Debugf("applypage", "m", m, "idx", idx) 262 263 if m.canNext { 264 err := m.Put(m.browse.NextSelector, m.browse.NextTitle) 265 if err != nil { 266 return err 267 } 268 } 269 if m.canPrevious { 270 err := m.Put(m.browse.PreviousSelector, m.browse.PreviousTitle) 271 if err != nil { 272 return err 273 } 274 } 275 return nil 276 } 277 278 // removes and returns the first of remaining menu options. 279 // fails if menu is empty. 280 func (m *Menu) shiftMenu() (string, string, error) { 281 if len(m.menu) == 0 { 282 return "", "", fmt.Errorf("menu is empty") 283 } 284 r := m.menu[0] 285 m.menu = m.menu[1:] 286 return r[0], r[1], nil 287 } 288 289 // prepare menu object for re-use. 290 func (m *Menu) reset() { 291 if m.browse.NextAvailable { 292 m.canNext = true 293 } 294 if m.browse.PreviousAvailable { 295 m.canPrevious = true 296 } 297 } 298 299 // Reset clears all current state from the menu object, making it ready for re-use in a new render. 300 func (m *Menu) Reset() { 301 m.menu = [][2]string{} 302 m.sink = false 303 m.reset() 304 }