go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

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 }