Get rid of annoying empty line in notcurses-view (and ncvisuals at offsets in general) Implement most of the Selector widget. Need to add styling and scrolling still. #166 Reenable ubuntu focal build Subtitles! We decode them, and display them in notcurses-view. If ncvisual_simple_streamer() is provided an extra ncplane, it will use it to display subtitles. #95 We now build Python by default, as things are working much better. ncplane_set_base() now takes channel, attrword, and EGC, so you can usually avoid having to set up and release a cell. ncplane_set_base_cell() takes over duty from ncplane_set_base() for ease of conversion. notcurses-demo and notcurses-view now both accept a 0 for delay multiplier, meaning 'go as fast as you possibly can'. Very small multipliers (e.g. 0.00001) no longer cause floating point exceptions. fading routines no longer cause floating point exceptions on very small timescales.pull/312/head
parent
3ee6f44831
commit
da0283ac25
@ -0,0 +1,24 @@
|
||||
% notcurses_selector(3)
|
||||
% nick black <nickblack@linux.com>
|
||||
% v1.1.2
|
||||
|
||||
# NAME
|
||||
|
||||
notcurses_selector - high level widget for selecting from a set
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
**#include <notcurses.h>**
|
||||
|
||||
```c
|
||||
```
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
# NOTES
|
||||
|
||||
# RETURN VALUES
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
**notcurses(3)**, **notcurses_ncplane(3)**
|
@ -0,0 +1,226 @@
|
||||
#include "notcurses.h"
|
||||
#include "internal.h"
|
||||
|
||||
// ideal body width given the ncselector's items and secondary/footer
|
||||
static size_t
|
||||
ncselector_body_width(const ncselector* n){
|
||||
size_t cols = 0;
|
||||
// the body is the maximum of
|
||||
// * longop + longdesc + 5
|
||||
// * secondary + 2
|
||||
// * footer + 2
|
||||
if(n->footer && strlen(n->footer) + 2 > cols){
|
||||
cols = strlen(n->footer) + 2;
|
||||
}
|
||||
if(n->secondary && strlen(n->secondary) + 2 > cols){
|
||||
cols = strlen(n->secondary) + 2;
|
||||
}
|
||||
if(n->longop + n->longdesc + 5 > cols){
|
||||
cols = n->longop + n->longdesc + 5;
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
// redraw the selector widget in its entirety
|
||||
static int
|
||||
ncselector_draw(ncselector* n){
|
||||
ncplane_erase(n->ncp);
|
||||
uint64_t channels = 0;
|
||||
channels_set_fg(&channels, 0x4040f0); // FIXME allow configuration
|
||||
// if we have a title, we'll draw a riser. the riser is two rows tall, and
|
||||
// exactly four columns longer than the title, and aligned to the right. we
|
||||
// draw a rounded box. the body will blow part or all of the bottom away.
|
||||
int yoff = 0;
|
||||
if(n->title){
|
||||
size_t riserwidth = strlen(n->title) + 4;
|
||||
int offx = ncplane_align(n->ncp, NCALIGN_RIGHT, riserwidth);
|
||||
ncplane_cursor_move_yx(n->ncp, 0, offx);
|
||||
ncplane_rounded_box_sized(n->ncp, 0, channels, 3, riserwidth, 0);
|
||||
ncplane_cursor_move_yx(n->ncp, 1, offx + 2);
|
||||
ncplane_putstr(n->ncp, n->title); // FIXME allow styling configuration
|
||||
yoff += 2;
|
||||
}
|
||||
size_t bodywidth = ncselector_body_width(n);
|
||||
int xoff = ncplane_align(n->ncp, NCALIGN_RIGHT, bodywidth);
|
||||
ncplane_cursor_move_yx(n->ncp, yoff, xoff);
|
||||
int dimy, dimx;
|
||||
ncplane_dim_yx(n->ncp, &dimy, &dimx);
|
||||
ncplane_rounded_box_sized(n->ncp, 0, channels, dimy - yoff, bodywidth, 0);
|
||||
unsigned printidx = n->startdisp;
|
||||
int bodyoffset = dimx - bodywidth + 2;
|
||||
for(yoff += 2 ; yoff < dimy - 2 ; ++yoff){
|
||||
if(printidx == n->selected){
|
||||
ncplane_styles_on(n->ncp, CELL_STYLE_REVERSE);
|
||||
}
|
||||
ncplane_printf_yx(n->ncp, yoff, bodyoffset, "%*.*s %s", (int)n->longop,
|
||||
(int)n->longop, n->items[printidx].option,
|
||||
n->items[printidx].desc);
|
||||
if(printidx == n->selected){
|
||||
ncplane_styles_off(n->ncp, CELL_STYLE_REVERSE);
|
||||
}
|
||||
++printidx;
|
||||
}
|
||||
return notcurses_render(n->ncp->nc);
|
||||
}
|
||||
|
||||
// calculate the necessary dimensions based off properties of the selector and
|
||||
// the containing screen FIXME should be based on containing ncplane
|
||||
static int
|
||||
ncselector_dim_yx(notcurses* nc, const ncselector* n, int* ncdimy, int* ncdimx){
|
||||
int rows = 0, cols = 0; // desired dimensions
|
||||
int dimy, dimx; // dimensions of containing screen
|
||||
notcurses_term_dim_yx(nc, &dimy, &dimx);
|
||||
if(n->title){ // header adds two rows for riser
|
||||
rows += 2;
|
||||
}
|
||||
// we have a top line, a bottom line, two lines of margin, and must be able
|
||||
// to display at least one row beyond that, so require five more
|
||||
rows += 5;
|
||||
if(rows > dimy){ // insufficient height to display selector
|
||||
return -1;
|
||||
}
|
||||
rows += n->itemcount - 1; // rows necessary to display all options
|
||||
if(rows > dimy){ // claw excess back
|
||||
rows = dimy;
|
||||
}
|
||||
*ncdimy = rows;
|
||||
cols = ncselector_body_width(n);
|
||||
// the riser, if it exists, is header + 4. the cols are the max of these two.
|
||||
if(n->title && strlen(n->title) + 4 > (size_t)cols){
|
||||
cols = strlen(n->title) + 4;
|
||||
}
|
||||
if(cols > dimx){ // insufficient width to display selector
|
||||
return -1;
|
||||
}
|
||||
*ncdimx = cols;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ncselector* ncselector_create(ncplane* n, int y, int x, const selector_options* opts){
|
||||
ncselector* ns = malloc(sizeof(*ns));
|
||||
ns->title = opts->title ? strdup(opts->title) : NULL;
|
||||
ns->secondary = opts->secondary ? strdup(opts->secondary) : NULL;
|
||||
ns->footer = opts->footer ? strdup(opts->footer) : NULL;
|
||||
ns->selected = 0;
|
||||
ns->startdisp = 0;
|
||||
ns->longop = 0;
|
||||
ns->longdesc = 0;
|
||||
if(opts->itemcount){
|
||||
if(!(ns->items = malloc(sizeof(*ns->items) * opts->itemcount))){
|
||||
free(ns->title); free(ns->secondary); free(ns->footer);
|
||||
free(n);
|
||||
return NULL;
|
||||
}
|
||||
}else{
|
||||
ns->items = NULL;
|
||||
}
|
||||
for(ns->itemcount = 0 ; ns->itemcount < opts->itemcount ; ++ns->itemcount){
|
||||
const struct selector_item* src = &opts->items[ns->itemcount];
|
||||
if(strlen(src->option) > ns->longop){
|
||||
ns->longop = strlen(src->option);
|
||||
}
|
||||
if(strlen(src->desc) > ns->longdesc){
|
||||
ns->longdesc = strlen(src->desc);
|
||||
}
|
||||
ns->items[ns->itemcount].option = strdup(src->option);
|
||||
ns->items[ns->itemcount].desc = strdup(src->desc);
|
||||
if(!(ns->items[ns->itemcount].desc && ns->items[ns->itemcount].option)){
|
||||
free(ns->items[ns->itemcount].option);
|
||||
free(ns->items[ns->itemcount].desc);
|
||||
goto freeitems;
|
||||
}
|
||||
}
|
||||
int dimy, dimx;
|
||||
if(ncselector_dim_yx(n->nc, ns, &dimy, &dimx)){
|
||||
goto freeitems;
|
||||
}
|
||||
if(!(ns->ncp = ncplane_new(n->nc, dimy, dimx, y, x, NULL))){
|
||||
goto freeitems;
|
||||
}
|
||||
ncselector_draw(ns); // deal with error here?
|
||||
return ns;
|
||||
|
||||
freeitems:
|
||||
while(ns->itemcount--){
|
||||
free(ns->items[ns->itemcount].option);
|
||||
free(ns->items[ns->itemcount].desc);
|
||||
}
|
||||
free(ns->items);
|
||||
free(ns->title); free(ns->secondary); free(ns->footer);
|
||||
free(ns);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ncselector* ncselector_aligned(ncplane* n, int y, ncalign_e align, const selector_options* opts);
|
||||
|
||||
int ncselector_additem(ncselector* n, const struct selector_item* item){
|
||||
size_t newsize = sizeof(*n->items) * (n->itemcount + 1);
|
||||
struct selector_item* items = realloc(n->items, newsize);
|
||||
if(!items){
|
||||
return -1;
|
||||
}
|
||||
n->items = items;
|
||||
n->items[n->itemcount].option = strdup(item->option);
|
||||
n->items[n->itemcount].desc = strdup(item->desc);
|
||||
++n->itemcount;
|
||||
return ncselector_draw(n);
|
||||
}
|
||||
|
||||
int ncselector_delitem(ncselector* n, const char* item){
|
||||
for(unsigned idx = 0 ; idx < n->itemcount ; ++idx){
|
||||
if(strcmp(n->items[idx].option, item) == 0){ // found it
|
||||
free(n->items[idx].option);
|
||||
free(n->items[idx].desc);
|
||||
if(idx < n->itemcount - 1){
|
||||
memmove(n->items + idx, n->items + idx + 1, sizeof(*n->items) * (n->itemcount - idx - 1));
|
||||
}else{
|
||||
if(idx){
|
||||
--n->selected;
|
||||
}
|
||||
}
|
||||
--n->itemcount;
|
||||
return ncselector_draw(n);
|
||||
}
|
||||
}
|
||||
return -1; // wasn't found
|
||||
}
|
||||
|
||||
void ncselector_previtem(ncselector* n, char** newitem){
|
||||
if(n->selected == 0){
|
||||
n->selected = n->itemcount;
|
||||
}
|
||||
--n->selected;
|
||||
if(newitem){
|
||||
*newitem = strdup(n->items[n->selected].option);
|
||||
}
|
||||
ncselector_draw(n);
|
||||
}
|
||||
|
||||
void ncselector_nextitem(ncselector* n, char** newitem){
|
||||
++n->selected;
|
||||
if(n->selected == n->itemcount){
|
||||
n->selected = 0;
|
||||
}
|
||||
if(newitem){
|
||||
*newitem = strdup(n->items[n->selected].option);
|
||||
}
|
||||
ncselector_draw(n);
|
||||
}
|
||||
|
||||
void ncselector_destroy(ncselector* n, char** item){
|
||||
if(n){
|
||||
if(item){
|
||||
*item = n->items[n->selected].option;
|
||||
n->items[n->selected].option = NULL;
|
||||
}
|
||||
while(n->itemcount--){
|
||||
free(n->items[n->itemcount].option);
|
||||
free(n->items[n->itemcount].desc);
|
||||
}
|
||||
free(n->items);
|
||||
free(n->title);
|
||||
free(n->secondary);
|
||||
free(n->footer);
|
||||
free(n);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <locale.h>
|
||||
#include <stdlib.h>
|
||||
#include <notcurses.h>
|
||||
|
||||
static struct selector_item items[] = {
|
||||
{ "first", "this is the first option", },
|
||||
{ "2nd", "this is the second option", },
|
||||
{ "3", "third, third, third option am i", },
|
||||
{ "fourth", "i have another option here", },
|
||||
{ "five", "golden rings", },
|
||||
{ "666", "now it is time for me to REIGN IN BLOOD", },
|
||||
{ "7seven7", "this monkey's gone to heaven", },
|
||||
{ "8 8 8", "the chinese love me, i'm told", },
|
||||
{ "nine", "nine, nine, nine 'cause you left me", },
|
||||
{ "ten", "stunning and brave", },
|
||||
};
|
||||
|
||||
int main(void){
|
||||
if(!setlocale(LC_ALL, "")){
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
notcurses_options opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
struct notcurses* nc = notcurses_init(&opts, stdout);
|
||||
if(nc == NULL){
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
selector_options sopts;
|
||||
memset(&sopts, 0, sizeof(sopts));
|
||||
sopts.maxdisplay = 4;
|
||||
sopts.items = items;
|
||||
sopts.itemcount = sizeof(items) / sizeof(*items);
|
||||
sopts.title = "this is an awfully long example of a selector title";
|
||||
ncplane_set_fg(notcurses_stdplane(nc), 0x40f040);
|
||||
ncplane_putstr_aligned(notcurses_stdplane(nc), 0, NCALIGN_RIGHT, "selector widget demo");
|
||||
struct ncselector* ns = ncselector_create(notcurses_stdplane(nc), 3, 0, &sopts);
|
||||
if(ns == NULL){
|
||||
notcurses_stop(nc);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
notcurses_render(nc);
|
||||
char32_t keypress;
|
||||
while((keypress = notcurses_getc_blocking(nc, NULL)) != (char32_t)-1){
|
||||
switch(keypress){
|
||||
case NCKEY_UP: case 'k': ncselector_previtem(ns, NULL); break;
|
||||
case NCKEY_DOWN: case 'j': ncselector_nextitem(ns, NULL); break;
|
||||
}
|
||||
if(keypress == 'q'){
|
||||
break;
|
||||
}
|
||||
notcurses_render(nc);
|
||||
}
|
||||
if(notcurses_stop(nc)){
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
#include "main.h"
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
TEST_CASE("SelectorTest") {
|
||||
if(getenv("TERM") == nullptr){
|
||||
return;
|
||||
}
|
||||
notcurses_options nopts{};
|
||||
nopts.inhibit_alternate_screen = true;
|
||||
nopts.suppress_banner = true;
|
||||
FILE* outfp_ = fopen("/dev/tty", "wb");
|
||||
REQUIRE(outfp_);
|
||||
struct notcurses* nc_ = notcurses_init(&nopts, outfp_);
|
||||
REQUIRE(nc_);
|
||||
struct ncplane* n_ = notcurses_stdplane(nc_);
|
||||
REQUIRE(n_);
|
||||
REQUIRE(0 == ncplane_cursor_move_yx(n_, 0, 0));
|
||||
|
||||
SUBCASE("EmptySelector") {
|
||||
struct selector_options opts{};
|
||||
struct ncselector* ncs = ncselector_create(notcurses_stdplane(nc_), 0, 0, &opts);
|
||||
REQUIRE(nullptr != ncs);
|
||||
CHECK(0 == notcurses_render(nc_));
|
||||
ncselector_destroy(ncs, nullptr);
|
||||
}
|
||||
|
||||
SUBCASE("TitledSelector") {
|
||||
struct selector_options opts{};
|
||||
opts.title = strdup("hey hey whaddya say");
|
||||
struct ncselector* ncs = ncselector_create(notcurses_stdplane(nc_), 0, 0, &opts);
|
||||
REQUIRE(nullptr != ncs);
|
||||
CHECK(0 == notcurses_render(nc_));
|
||||
ncselector_destroy(ncs, nullptr);
|
||||
}
|
||||
|
||||
SUBCASE("SecondarySelector") {
|
||||
struct selector_options opts{};
|
||||
opts.secondary = strdup("this is not a title, but it's not *not* a title");
|
||||
struct ncselector* ncs = ncselector_create(notcurses_stdplane(nc_), 0, 0, &opts);
|
||||
REQUIRE(nullptr != ncs);
|
||||
CHECK(0 == notcurses_render(nc_));
|
||||
ncselector_destroy(ncs, nullptr);
|
||||
}
|
||||
|
||||
SUBCASE("FooterSelector") {
|
||||
struct selector_options opts{};
|
||||
opts.secondary = strdup("i am a lone footer, little old footer");
|
||||
struct ncselector* ncs = ncselector_create(notcurses_stdplane(nc_), 0, 0, &opts);
|
||||
REQUIRE(nullptr != ncs);
|
||||
CHECK(0 == notcurses_render(nc_));
|
||||
ncselector_destroy(ncs, nullptr);
|
||||
}
|
||||
|
||||
SUBCASE("PopulatedSelector") {
|
||||
selector_item items[] = {
|
||||
{ strdup("op1"), strdup("this is option 1"), },
|
||||
{ strdup("2ndop"), strdup("this is option #2"), },
|
||||
{ strdup("tres"), strdup("option the third"), },
|
||||
};
|
||||
struct selector_options opts{};
|
||||
opts.items = items;
|
||||
opts.itemcount = sizeof(items) / sizeof(*items);
|
||||
struct ncselector* ncs = ncselector_create(notcurses_stdplane(nc_), 0, 0, &opts);
|
||||
REQUIRE(nullptr != ncs);
|
||||
CHECK(0 == notcurses_render(nc_));
|
||||
ncselector_destroy(ncs, nullptr);
|
||||
}
|
||||
|
||||
CHECK(0 == notcurses_stop(nc_));
|
||||
CHECK(0 == fclose(outfp_));
|
||||
}
|
Loading…
Reference in New Issue