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