go-vise

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

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 }