diff --git a/COPYRIGHT b/COPYRIGHT index 89a9e65b0..14e73ed41 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -2,6 +2,7 @@ Copyright 2019-2021 Nick Black Copyright 2019-2021 Marek Habersack Copyright 2020-2021 José Luis Cruz Copyright 2020-2021 igo95862 +Copyright 2021 Łukasz Drukała Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/USAGE.md b/USAGE.md index f4e7a346b..8b92b0d24 100644 --- a/USAGE.md +++ b/USAGE.md @@ -9,7 +9,7 @@ to backwards compatibility. * [Planes](#planes) ([Plane Channels API](#plane-channels-api)) * [Cells](#cells) ([Cell Channels API](#cell-channels-api)) * [Reels](#reels) ([ncreel Examples](#ncreel-examples)) -* [Widgets](#widgets) ([Plots](#plots)) ([Readers](#readers)) ([Progbars](#progbars)) +* [Widgets](#widgets) ([Plots](#plots)) ([Readers](#readers)) ([Progbars](#progbars)) ([Tabs](#tabs)) * [Channels](#channels) * [Visuals](#visuals) ([QR codes](#qrcodes)) ([Multimedia](#multimedia)) ([Pixels](#pixels)) * [Stats](#stats) @@ -2486,7 +2486,7 @@ the time of each redraw), with the horizontal length scaled by 2 for purposes of comparison. I.e. for a plane of 20 rows and 50 columns, the progress will be to the right (50 > 40) or left with `OPTION_RETROGRADE`. -``` +```c // Takes ownership of the ncplane 'n', which will be destroyed by // ncprogbar_destroy(). The progress bar is initially at 0%. struct ncprogbar* ncprogbar_create(struct ncplane* n, const ncprogbar_options* opts); @@ -2514,6 +2514,189 @@ double ncprogbar_progress(const struct ncprogbar* n); void ncprogbar_destroy(struct ncprogbar* n); ``` +### Tabs + +Tabbed widgets. The tab list is displayed at the top or at the bottom of the +plane, and only one tab is visible at a time. + +```c +// Display the tab list at the bottom instead of at the top of the plane +#define NCTABBED_OPTION_BOTTOM 0x0001ull + +typedef struct nctabbed_options { + uint64_t selchan; // channel for the selected tab header + uint64_t hdrchan; // channel for unselected tab headers + uint64_t sepchan; // channel for the tab separator + char* separator; // separator string + uint64_t flags; // bitmask of NCTABBED_OPTION_* +} nctabbed_options; + +// Tab content drawing callback. Takes the tab it was associated to, the ncplane +// on which tab content is to be drawn, and the user pointer of the tab. +// It is called during nctabbed_redraw(). +typedef void (*tabcb)(struct nctab* t, struct ncplane* ncp, void* curry); + +// Creates a new nctabbed widget, associated with the given ncplane 'n', and with +// additional options given in 'opts'. When 'opts' is NULL, it acts as if it were +// called with an all-zero opts. The widget takes ownership of 'n', and destroys +// it when the widget is destroyed. Returns the newly created widget. Returns +// NULL on failure, also destroying 'n'. +struct nctabbed* nctabbed_create(struct ncplane* n, const nctabbed_options* opts); + +// Destroy an nctabbed widget. All memory belonging to 'nt' is deallocated, +// including all tabs and their names. The plane associated with 'nt' is also +// destroyed. Calling this with NULL does nothing. +void nctabbed_destroy(struct nctabbed* nt); + +// Redraw the widget. This calls the tab callback of the currently selected tab +// to draw tab contents, and draws tab headers. The tab content plane is not +// modified by this function, apart from resizing the plane is necessary. +void nctabbed_redraw(struct nctabbed* nt); + +// Make sure the tab header of the currently selected tab is at least partially +// visible. (by rotating tabs until at least one column is displayed) +// Does nothing if there are no tabs. +void nctabbed_ensure_selected_header_visible(struct nctabbed* nt); + +// Returns the currently selected tab, or NULL if there are no tabs. +struct nctab* nctabbed_selected(struct nctabbed* nt); + +// Returns the leftmost tab, or NULL if there are no tabs. +struct nctab* nctabbed_leftmost(struct nctabbed* nt); + +// Returns the number of tabs in the widget. +int nctabbed_tabcount(struct nctabbed* nt); + +// Returns the plane associated to 'nt'. +struct ncplane* nctabbed_plane(struct nctabbed* nt); + +// Returns the tab content plane. +struct ncplane* nctabbed_content_plane(struct nctabbed* nt); + +// Returns the tab callback. +tabcb nctab_cb(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the tab name. This is not a copy and it should not be stored. +const char* nctab_name(struct nctab* t); + +// Returns the width (in columns) of the tab's name. +int nctab_name_width(struct nctab* t); + +// Returns the tab's user pointer. +void* nctab_userptr(struct nctab* t); + +// Returns the tab to the right of 't'. This does not change which tab is selected. +struct nctab* nctab_next(struct nctab* t); + +// Returns the tab to the left of 't'. This does not change which tab is selected. +struct nctab* nctab_prev(struct nctab* t); + +// Add a new tab to 'nt' with the given tab callback, name, and user pointer. +// If both 'before' and 'after' are NULL, the tab is inserted after the selected +// tab. Otherwise, it gets put after 'after' (if not NULL) and before 'before' +// (if not NULL). If both 'after' and 'before' are given, they must be two +// neighboring tabs (the tab list is circular, so the last tab is immediately +// before the leftmost tab), otherwise the function returns NULL. If 'name' is +// NULL or a string containing illegal characters, the function returns NULL. +// On all other failures the function also returns NULL. If it returns NULL, +// none of the arguments are modified, and the widget state is not altered. +struct nctab* nctabbed_add(struct nctabbed* nt, struct nctab* after, + struct nctab* before, tabcb tcb, + const char* name, void* opaque); + +// Remove a tab 't' from 'nt'. Its neighboring tabs become neighbors to each +// other. If 't' if the selected tab, the tab after 't' becomes selected. +// Likewise if 't' is the leftmost tab, the tab after 't' becomes leftmost. +// If 't' is the only tab, there will no more be a selected or leftmost tab, +// until a new tab is added. Returns -1 if 't' is NULL, and 0 otherwise. +int nctabbed_del(struct nctabbed* nt, struct nctab* t); + +// Move 't' after 'after' (if not NULL) and before 'before' (if not NULL). +// If both 'after' and 'before' are NULL, the function returns -1, otherwise +// it returns 0. +int nctab_move(struct nctabbed* nt, struct nctab* t, struct nctab* after, + struct nctab* before); + +// Move 't' to the right by one tab, looping around to become leftmost if needed. +void nctab_move_right(struct nctabbed* nt, struct nctab* t); + +// Move 't' to the right by one tab, looping around to become the last tab if needed. +void nctab_move_left(struct nctabbed* nt, struct nctab* t); + +// Rotate the tabs of 'nt' right by 'amt' tabs, or '-amt' tabs left if 'amt' is +// negative. Tabs are rotated only by changing the leftmost tab; the selected tab +// stays the same. If there are no tabs, nothing happens. +void nctabbed_rotate(struct nctabbed* nt, int amt); + +// Select the tab after the currently selected tab, and return the newly selected +// tab. Returns NULL if there are no tabs. +struct nctab* nctabbed_next(struct nctabbed* nt); + +// Select the tab before the currently selected tab, and return the newly selected +// tab. Returns NULL if there are no tabs. +struct nctab* nctabbed_prev(struct nctabbed* nt); + +// Change the selected tab to be 't'. Returns the previously selected tab. +struct nctab* nctabbed_select(struct nctabbed* nt, struct nctab* t); + +// Write the channels for tab headers, the selected tab header, and the separator +// to '*hdrchan', '*selchan', and '*sepchan' respectively. +void nctabbed_channels(struct nctabbed* nt, uint64_t* RESTRICT hdrchan, + uint64_t* RESTRICT selchan, uint64_t* RESTRICT sepchan); + +static inline uint64_t +nctabbed_hdrchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, &ch, NULL, NULL); + return ch; +} + +static inline uint64_t +nctabbed_selchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, NULL, &ch, NULL); + return ch; +} + +static inline uint64_t +nctabbed_sepchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, NULL, NULL, &ch); + return ch; +} + +// Returns the tab separator. This is not a copy and it should not be stored. +// This can be NULL, if the separator was set to NULL in ncatbbed_create() or +// nctabbed_set_separator(). +const char* nctabbed_separator(struct nctabbed* nt); + +// Returns the tab separator width, or zero if there is no separator. +int nctabbed_separator_width(struct nctabbed* nt); + +// Set the tab headers channel for 'nt'. +void nctabbed_set_hdrchan(struct nctabbed* nt, uint64_t chan); + +// Set the selected tab header channel for 'nt'. +void nctabbed_set_selchan(struct nctabbed* nt, uint64_t chan); + +// Set the tab separator channel for 'nt'. +void nctabbed_set_sepchan(struct nctabbed* nt, uint64_t chan); + +// Set the tab callback function for 't'. Returns the previous tab callback. +tabcb nctab_set_cb(struct nctab* t, tabcb newcb); + +// Change the name of 't'. Returns -1 if 'newname' is NULL, and 0 otherwise. +int nctab_set_name(struct nctab* t, const char* newname); + +// Set the user pointer of 't'. Returns the previous user pointer. +void* nctab_set_userptr(struct nctab* t, void* newopaque); + +// Change the tab separator for 'nt'. Returns -1 if 'separator' is not NULL and +// is not a valid string, and 0 otherwise. +int nctabbed_set_separator(struct nctabbed* nt, const char* separator); +``` + ## Channels A channel encodes 24 bits of RGB color, using 8 bits for each component. It diff --git a/doc/man/index.html b/doc/man/index.html index 558ec1d75..5d3da7e4e 100644 --- a/doc/man/index.html +++ b/doc/man/index.html @@ -73,6 +73,7 @@ notcurses_stats—notcurses runtime statistics
notcurses_stdplane—acquire the standard ncplane
notcurses_stop—collapse the context
+ notcurses_tabbed—tabbed interface widget
notcurses_tree—line-based widget for hierarchical data
notcurses_visual—operations on ncvisual objects
diff --git a/doc/man/man3/notcurses.3.md b/doc/man/man3/notcurses.3.md index 4218d6f94..0c4f6ef5b 100644 --- a/doc/man/man3/notcurses.3.md +++ b/doc/man/man3/notcurses.3.md @@ -117,6 +117,7 @@ A few high-level widgets are included, all built atop ncplanes: * **notcurses_progbar(3)** for drawing progress bars * **notcurses_reader(3)** for free-form input data * **notcurses_reel(3)** for hierarchal display of block-based data +* **notcurses_tabbed(3)** for tabbed interfaces * **notcurses_selector(3)** for selecting one item from a set * **notcurses_tree(3)** for hierarchal display of line-based data @@ -187,6 +188,7 @@ order to turn most error returns into exceptions. **notcurses_stats(3)**, **notcurses_stdplane(3)**, **notcurses_stop(3)**, +**notcurses_tabbed(3)**, **notcurses_tree(3)**, **notcurses_visual(3)**, **terminfo(5)**, **ascii(7)**, **utf-8(7)**, diff --git a/doc/man/man3/notcurses_tabbed.3.md b/doc/man/man3/notcurses_tabbed.3.md new file mode 100644 index 000000000..0d0621f13 --- /dev/null +++ b/doc/man/man3/notcurses_tabbed.3.md @@ -0,0 +1,161 @@ +% notcurses_tabbed(3) +% v2.2.3 + +# NAME + +notcurses_tabbed – tabbed interface widget + +# SYNOPSIS + +**#include ** + +```c +#define NCTABBED_OPTION_BOTTOM 0x0001 + +struct nctabbed; +struct ncplane; +struct nctab; + +typedef struct nctabbed_options { + uint64_t selchan; // channel for the selected tab header + uint64_t hdrchan; // channel for unselected tab headers + uint64_t sepchan; // channel for the tab separator + char* separator; // separator string (copied by nctabbed_create()) + uint64_t flags; // bitmask of NCTABBED_OPTION_* +} nctabbed_options; + +typedef void (*tabcb)(struct nctab* t, struct ncplane* ncp, void* userptr); +``` + +**struct nctabbed* nctabbed_create(struct ncplane* ***ncp***, const nctabbed_options* ***opts***);** + +**void nctabbed_destroy(struct nctabbed* ***nt***);** + +**void nctabbed_redraw(struct nctabbed* ***nt***);** + +**void nctabbed_ensure_selected_header_visible(struct nctabbed* ***nt***);** + +**struct nctab* nctabbed_selected(struct nctabbed* ***nt***);** + +**struct nctab* nctabbed_leftmost(struct nctabbed* ***nt***);** + +**int nctabbed_tabcount(struct nctabbed* ***nt***);** + +**struct ncplane* nctabbed_plane(struct nctabbed* ***nt***);** + +**struct ncplane* nctabbed_content_plane(struct nctabbed* ***nt***);** + +**tabcb nctab_cb(struct nctab* ***t***);** + +**const char* nctab_name(struct nctab* ***t***);** + +**int nctab_name_width(struct nctab* ***t***);** + +**void* nctab_userptr(struct nctab* ***t***);** + +**struct nctab* nctab_next(struct nctab* ***t***);** + +**struct nctab* nctab_prev(struct nctab* ***t***);** + +**struct nctab* nctabbed_add(struct nctabbed* ***nt***, struct nctab* ***after***, struct nctab* ***before***, tabcb ***tcb***, const char* ***name***, void* ***opaque***); + +**int nctabbed_del(struct nctabbed* ***nt***, struct nctab* ***t***);** + +**int nctab_move(struct nctabbed* ***nt***, struct nctab* ***t***, struct nctab* ***after***, + struct nctab* ***before***);** + +**void nctab_move_right(struct nctabbed* ***nt***, struct nctab* ***t***);** + +**void nctab_move_left(struct nctabbed* ***nt***, struct nctab* ***t***);** + +**void nctabbed_rotate(struct nctabbed* ***nt***, int ***amt***);** + +**struct nctab* nctabbed_next(struct nctabbed* ***nt***);** + +**struct nctab* nctabbed_prev(struct nctabbed* ***nt***);** + +**struct nctab* nctabbed_select(struct nctabbed* ***nt***, struct nctab* ***t***);** + +**void nctabbed_channels(struct nctabbed* ***nt***, uint64_t* RESTRICT ***hdrchan***, uint64_t* RESTRICT ***selchan***, uint64_t* RESTRICT ***sepchan***);** + +**uint64_t nctabbed_hdrchan(struct nctabbed* ***nt***);** + +**uint64_t nctabbed_selchan(struct nctabbed* ***nt***);** + +**uint64_t nctabbed_sepchan(struct nctabbed* ***nt***);** + +**const char* nctabbed_separator(struct nctabbed* ***nt***);** + +**int nctabbed_separator_width(struct nctabbed* ***nt***);** + +**void nctabbed_set_hdrchan(struct nctabbed* ***nt***, uint64_t ***chan***);** + +**void nctabbed_set_selchan(struct nctabbed* ***nt***, uint64_t ***chan***);** + +**void nctabbed_set_sepchan(struct nctabbed* ***nt***, uint64_t ***chan***);** + +**tabcb nctab_set_cb(struct nctab* ***t***, tabcb ***newcb***);** + +**int nctab_set_name(struct nctab* ***t***, const char* ***newname***);** + +**void* nctab_set_userptr(struct nctab* ***t***, void* ***newopaque***);** + +**int nctabbed_set_separator(struct nctabbed* ***nt***, const char* ***separator***);** + +# DESCRIPTION + +An **nctabbed** is a widget with one area for data display and a bar with +names of tabs. Unless there are no tabs, exactly one tab is "selected", and +exactly one tab is "leftmost". The selected tab is the one that controls +the tab content plane. The leftmost tab is the one of which the header is +visible furthest to the left. Any tab can be moved to and from anywhere in the +list. The tabs can be "rotated", which really means the leftmost tab gets +shifted. The widget is drawn only when **nctabbed_redraw** or **ntabbed_create** +are called. + +## LAYOUT + +The widget has a tab list either on the top or the bottom, 1 row thick. The tab +list contains tab headers (i.e. their names), separated with the separator +specified in **nctabbed_create** or **nctabbed_set_separator**. The channels +for the selected tab's header, other tab headers and the separator can be +set independently of each other. The tab separator can be 0-length, or NULL, +in which case there is no visible separator between tab headers. The selected +tab can be made sure to be visible when drawn (by changing the leftmost tab) +by calling the very long-named **nctabbed_ensure_selected_header_visible**. +The rest of the widget is an **ncplane** housing the selected tab content. (if any) + +## THE TAB CALLBACK + +The tab callback (of type **tabcb**) takes a tab, the tab content plane, and +the opaque pointer given to **nctabbed_add** or **nctabbed_set_userptr**. +It is called when the tab content is supposed to be drawn, that is when +the whole widget is redrawn. It should draw the tab content and possibly +make other actions, but it should not assume anything about the current state +of the tab content plane, nor should it modify the widget's or the tab's state. + +# RETURN VALUES + +**nctabbed_create** returns the newly created widget, or **NULL** when the widget +failed to be created. This destroys the **ncplane** given to it even if it fails. + +**nctabbed_selected** and **nctabbed_leftmost** return the selected and +leftmost tabs, respectively. If there are no tabs, these return **NULL**. + +**nctab_name** returns the tab's name. This is not a copy, and it should not be +stored, since it is freed when the tab's name is changed or the tab is deleted. + +**nctabbed_next**, **nctabbed_prev** and **nctabbed_select** return the newly +selected tab. + +**nctabbed_separator** returns the tab separator. This is not a copy, and it +should not be stored, since it is freed when the separator is changed or the +widget is deleted. + +Functions returning **int** return **-1** on failure. + +# SEE ALSO + +**notcurses(3)**, +**notcurses_channels(3)**, +**notcurses_plane(3)** diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index 3d624c335..1f328abe6 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -55,6 +55,8 @@ struct ncreader; // widget supporting free string input ala readline struct ncfadectx; // context for a palette fade operation struct nctablet; // grouped item within an ncreel struct ncreel; // hierarchical block-based data browser +struct nctab; // grouped item within an nctabbed +struct nctabbed; // widget with one tab visible at a time // we never blit full blocks, but instead spaces (more efficient) with the // background set to the desired foreground. @@ -3188,6 +3190,217 @@ API double ncprogbar_progress(const struct ncprogbar* n) // Destroy the progress bar and its underlying ncplane. API void ncprogbar_destroy(struct ncprogbar* n); +// Tabbed widgets. The tab list is displayed at the top or at the bottom of the +// plane, and only one tab is visible at a time. + +// Display the tab list at the bottom instead of at the top of the plane +#define NCTABBED_OPTION_BOTTOM 0x0001ull + +typedef struct nctabbed_options { + uint64_t selchan; // channel for the selected tab header + uint64_t hdrchan; // channel for unselected tab headers + uint64_t sepchan; // channel for the tab separator + char* separator; // separator string (copied by nctabbed_create()) + uint64_t flags; // bitmask of NCTABBED_OPTION_* +} nctabbed_options; + +// Tab content drawing callback. Takes the tab it was associated to, the ncplane +// on which tab content is to be drawn, and the user pointer of the tab. +// It is called during nctabbed_redraw(). +typedef void (*tabcb)(struct nctab* t, struct ncplane* ncp, void* curry); + +// Creates a new nctabbed widget, associated with the given ncplane 'n', and with +// additional options given in 'opts'. When 'opts' is NULL, it acts as if it were +// called with an all-zero opts. The widget takes ownership of 'n', and destroys +// it when the widget is destroyed. Returns the newly created widget. Returns +// NULL on failure, also destroying 'n'. +API ALLOC struct nctabbed* nctabbed_create(struct ncplane* n, const nctabbed_options* opts) + __attribute ((nonnull (1))); + +// Destroy an nctabbed widget. All memory belonging to 'nt' is deallocated, +// including all tabs and their names. The plane associated with 'nt' is also +// destroyed. Calling this with NULL does nothing. +API void nctabbed_destroy(struct nctabbed* nt); + +// Redraw the widget. This calls the tab callback of the currently selected tab +// to draw tab contents, and draws tab headers. The tab content plane is not +// modified by this function, apart from resizing the plane is necessary. +API void nctabbed_redraw(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Make sure the tab header of the currently selected tab is at least partially +// visible. (by rotating tabs until at least one column is displayed) +// Does nothing if there are no tabs. +API void nctabbed_ensure_selected_header_visible(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the currently selected tab, or NULL if there are no tabs. +API struct nctab* nctabbed_selected(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the leftmost tab, or NULL if there are no tabs. +API struct nctab* nctabbed_leftmost(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the number of tabs in the widget. +API int nctabbed_tabcount(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the plane associated to 'nt'. +API struct ncplane* nctabbed_plane(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the tab content plane. +API struct ncplane* nctabbed_content_plane(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the tab callback. +API tabcb nctab_cb(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the tab name. This is not a copy and it should not be stored. +API const char* nctab_name(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the width (in columns) of the tab's name. +API int nctab_name_width(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the tab's user pointer. +API void* nctab_userptr(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the tab to the right of 't'. This does not change which tab is selected. +API struct nctab* nctab_next(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Returns the tab to the left of 't'. This does not change which tab is selected. +API struct nctab* nctab_prev(struct nctab* t) + __attribute__ ((nonnull (1))); + +// Add a new tab to 'nt' with the given tab callback, name, and user pointer. +// If both 'before' and 'after' are NULL, the tab is inserted after the selected +// tab. Otherwise, it gets put after 'after' (if not NULL) and before 'before' +// (if not NULL). If both 'after' and 'before' are given, they must be two +// neighboring tabs (the tab list is circular, so the last tab is immediately +// before the leftmost tab), otherwise the function returns NULL. If 'name' is +// NULL or a string containing illegal characters, the function returns NULL. +// On all other failures the function also returns NULL. If it returns NULL, +// none of the arguments are modified, and the widget state is not altered. +API ALLOC struct nctab* nctabbed_add(struct nctabbed* nt, struct nctab* after, + struct nctab* before, tabcb tcb, + const char* name, void* opaque) + __attribute__ ((nonnull (1, 5))); + +// Remove a tab 't' from 'nt'. Its neighboring tabs become neighbors to each +// other. If 't' if the selected tab, the tab after 't' becomes selected. +// Likewise if 't' is the leftmost tab, the tab after 't' becomes leftmost. +// If 't' is the only tab, there will no more be a selected or leftmost tab, +// until a new tab is added. Returns -1 if 't' is NULL, and 0 otherwise. +API int nctabbed_del(struct nctabbed* nt, struct nctab* t) + __attribute__ ((nonnull (1))); + +// Move 't' after 'after' (if not NULL) and before 'before' (if not NULL). +// If both 'after' and 'before' are NULL, the function returns -1, otherwise +// it returns 0. +API int nctab_move(struct nctabbed* nt, struct nctab* t, struct nctab* after, + struct nctab* before) + __attribute__ ((nonnull (1, 2))); + +// Move 't' to the right by one tab, looping around to become leftmost if needed. +API void nctab_move_right(struct nctabbed* nt, struct nctab* t) + __attribute__ ((nonnull (1, 2))); + +// Move 't' to the right by one tab, looping around to become the last tab if needed. +API void nctab_move_left(struct nctabbed* nt, struct nctab* t) + __attribute__ ((nonnull (1, 2))); + +// Rotate the tabs of 'nt' right by 'amt' tabs, or '-amt' tabs left if 'amt' is +// negative. Tabs are rotated only by changing the leftmost tab; the selected tab +// stays the same. If there are no tabs, nothing happens. +API void nctabbed_rotate(struct nctabbed* nt, int amt) + __attribute__ ((nonnull (1))); + +// Select the tab after the currently selected tab, and return the newly selected +// tab. Returns NULL if there are no tabs. +API struct nctab* nctabbed_next(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Select the tab before the currently selected tab, and return the newly selected +// tab. Returns NULL if there are no tabs. +API struct nctab* nctabbed_prev(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Change the selected tab to be 't'. Returns the previously selected tab. +API struct nctab* nctabbed_select(struct nctabbed* nt, struct nctab* t) + __attribute__ ((nonnull (1, 2))); + +// Write the channels for tab headers, the selected tab header, and the separator +// to '*hdrchan', '*selchan', and '*sepchan' respectively. +API void nctabbed_channels(struct nctabbed* nt, uint64_t* RESTRICT hdrchan, + uint64_t* RESTRICT selchan, uint64_t* RESTRICT sepchan) + __attribute__ ((nonnull (1))); + +static inline uint64_t +nctabbed_hdrchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, &ch, NULL, NULL); + return ch; +} + +static inline uint64_t +nctabbed_selchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, NULL, &ch, NULL); + return ch; +} + +static inline uint64_t +nctabbed_sepchan(struct nctabbed* nt){ + uint64_t ch; + nctabbed_channels(nt, NULL, NULL, &ch); + return ch; +} + +// Returns the tab separator. This is not a copy and it should not be stored. +// This can be NULL, if the separator was set to NULL in ncatbbed_create() or +// nctabbed_set_separator(). +API const char* nctabbed_separator(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Returns the tab separator width, or zero if there is no separator. +API int nctabbed_separator_width(struct nctabbed* nt) + __attribute__ ((nonnull (1))); + +// Set the tab headers channel for 'nt'. +API void nctabbed_set_hdrchan(struct nctabbed* nt, uint64_t chan) + __attribute__ ((nonnull (1))); + +// Set the selected tab header channel for 'nt'. +API void nctabbed_set_selchan(struct nctabbed* nt, uint64_t chan) + __attribute__ ((nonnull (1))); + +// Set the tab separator channel for 'nt'. +API void nctabbed_set_sepchan(struct nctabbed* nt, uint64_t chan) + __attribute__ ((nonnull (1))); + +// Set the tab callback function for 't'. Returns the previous tab callback. +API tabcb nctab_set_cb(struct nctab* t, tabcb newcb) + __attribute__ ((nonnull (1))); + +// Change the name of 't'. Returns -1 if 'newname' is NULL, and 0 otherwise. +API int nctab_set_name(struct nctab* t, const char* newname) + __attribute__ ((nonnull (1, 2))); + +// Set the user pointer of 't'. Returns the previous user pointer. +API void* nctab_set_userptr(struct nctab* t, void* newopaque) + __attribute__ ((nonnull (1))); + +// Change the tab separator for 'nt'. Returns -1 if 'separator' is not NULL and +// is not a valid string, and 0 otherwise. +API int nctabbed_set_separator(struct nctabbed* nt, const char* separator) + __attribute__ ((nonnull (1, 2))); + // Plots. Given a rectilinear area, an ncplot can graph samples along some axis. // There is some underlying independent variable--this could be e.g. measurement // sequence number, or measurement time. Samples are tagged with this variable, which diff --git a/src/lib/internal.h b/src/lib/internal.h index a2419ae48..a25b84376 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -256,6 +256,28 @@ typedef struct ncprogbar { bool retrograde; } ncprogbar; +typedef struct nctab { + struct nctabbed* nt; // The nctabbed this belongs to + tabcb cb; // tab callback + char* name; // tab name + int namecols; // tab name width in columns + void* curry; // user pointer + struct nctab* prev; + struct nctab* next; +} nctab; + +typedef struct nctabbed { + ncplane* ncp; // widget ncplane + ncplane* p; // tab content ncplane + ncplane* hp; // tab headers ncplane + // a doubly-linked circular list of tabs + nctab* leftmost; // the tab most to the left + nctab* selected; // the currently selected tab + int tabcount; // tab separator (can be NULL) + int sepcols; // separator with in columns + nctabbed_options opts; // copied in nctabbed_create() +} nctabbed; + // terminfo cache typedef struct tinfo { unsigned colors;// number of colors terminfo reported usable for this screen diff --git a/src/lib/tabbed.c b/src/lib/tabbed.c new file mode 100644 index 000000000..c0bb7dae5 --- /dev/null +++ b/src/lib/tabbed.c @@ -0,0 +1,456 @@ +#include "internal.h" + +void nctabbed_redraw(nctabbed* nt){ + nctab* t; + int drawn_cols = 0; + int rows, cols; + if(nt->tabcount == 0){ + // no tabs = nothing to draw + ncplane_erase(nt->hp); + return; + } + // update sizes for planes + ncplane_dim_yx(nt->ncp, &rows, &cols); + if(nt->opts.flags & NCTABBED_OPTION_BOTTOM){ + ncplane_resize_simple(nt->hp, -1, cols); + ncplane_resize_simple(nt->p, rows - 1, cols); + ncplane_move_yx(nt->hp, rows - 2, 0); + }else{ + ncplane_resize_simple(nt->hp, -1, cols); + ncplane_resize_simple(nt->p, rows - 1, cols); + } + // the callback draws the tab contents + if(nt->selected->cb){ + nt->selected->cb(nt->selected, nt->p, nt->selected->curry); + } + // now we draw the headers + t = nt->leftmost; + ncplane_erase(nt->hp); + ncplane_set_channels(nt->hp, nt->opts.hdrchan); + do{ + if(t == nt->selected){ + ncplane_set_channels(nt->hp, nt->opts.selchan); + drawn_cols += ncplane_putstr(nt->hp, t->name); + ncplane_set_channels(nt->hp, nt->opts.hdrchan); + }else{ + drawn_cols += ncplane_putstr(nt->hp, t->name); + } + // avoid drawing the separator after the last tab, or when we + // ran out of space, or when it's not set + if((t->next != nt->leftmost || drawn_cols >= cols) && nt->opts.separator){ + ncplane_set_channels(nt->hp, nt->opts.sepchan); + drawn_cols += ncplane_putstr(nt->hp, nt->opts.separator); + ncplane_set_channels(nt->hp, nt->opts.hdrchan); + } + t = t->next; + }while(t != nt->leftmost && drawn_cols < cols); +} + +void nctabbed_ensure_selected_header_visible(nctabbed* nt){ + nctab* t = nt->leftmost; + int cols = ncplane_dim_x(nt->hp); + int takencols = 0; + if(!t){ + return; + } +//fprintf(stderr, "ensuring selected header visible\n"); + do{ + if(t == nt->selected){ + break; + } + takencols += t->namecols + nt->sepcols; + if(takencols >= cols){ +//fprintf(stderr, "not enough space, rotating\n"); + takencols -= nt->leftmost->namecols + nt->sepcols; + nctabbed_rotate(nt, -1); + } + t = t->next; +//fprintf(stderr, "iteration over: takencols = %d, cols = %d\n", takencols, cols); + }while(t != nt->leftmost); +//fprintf(stderr, "ensuring done\n"); +} + +static bool +nctabbed_validate_opts(ncplane* n, const nctabbed_options* opts){ + notcurses* nc = ncplane_notcurses(n); + if(opts->flags > NCTABBED_OPTION_BOTTOM){ + logwarn(nc, "Provided unsupported flags 0x%016jx\n", (uint64_t)opts->flags); + } + if(opts->sepchan && !opts->separator){ + logwarn(nc, "Provided non-zero separator channel when separator is NULL") + } + return true; +} + +nctab* nctabbed_selected(nctabbed* nt){ + return nt->selected; +} + +nctab* nctabbed_leftmost(nctabbed* nt){ + return nt->leftmost; +} + +int nctabbed_tabcount(nctabbed* nt){ + return nt->tabcount; +} + +ncplane* nctabbed_plane(nctabbed* nt){ + return nt->ncp; +} + +ncplane* nctabbed_content_plane(nctabbed* nt){ + return nt->p; +} + +tabcb nctab_cb(nctab* t){ + return t->cb; +} + +const char* nctab_name(nctab* t){ + return t->name; +} + +int nctab_name_width(nctab* t){ + return t->namecols; +} + +void* nctab_userptr(nctab* t){ + return t->curry; +} + +nctab* nctab_next(nctab* t){ + return t->next; +} + +nctab* nctab_prev(nctab* t){ + return t->prev; +} + +nctabbed* nctabbed_create(ncplane* n, const nctabbed_options* topts){ + nctabbed_options zeroed = {}; + ncplane_options nopts = {}; + int nrows, ncols; + nctabbed* nt; + notcurses* nc = ncplane_notcurses(n); + if(!topts){ + topts = &zeroed; + } + if(!nctabbed_validate_opts(n, topts)){ + return NULL; + } + if((nt = malloc(sizeof(*nt))) == NULL){ + logerror(nc, "Couldn't allocate nctabbed"); + return NULL; + } + nt->ncp = n; + nt->leftmost = nt->selected = NULL; + nt->tabcount = 0; + memcpy(&nt->opts, topts, sizeof(*topts)); + if(nt->opts.separator){ + if((nt->opts.separator = strdup(nt->opts.separator)) == NULL){ + logerror(nc, "Couldn't allocate nctabbed separator"); + free(nt); + return NULL; + } + if((nt->sepcols = ncstrwidth(nt->opts.separator)) < 0){ + logerror(nc, "Separator string contains illegal characters"); + free(nt->opts.separator); + free(nt); + return NULL; + } + }else{ + nt->sepcols = 0; + } + ncplane_dim_yx(n, &nrows, &ncols); + if(topts->flags & NCTABBED_OPTION_BOTTOM){ + nopts.y = nopts.x = 0; + nopts.cols = ncols; + nopts.rows = nrows - 1; + if((nt->p = ncplane_create(n, &nopts)) == NULL){ + logerror(nc, "Couldn't create the tab content plane"); + ncplane_genocide(n); + free(nt); + return NULL; + } + nopts.y = nrows - 2; + nopts.rows = 1; + if((nt->hp = ncplane_create(n, &nopts)) == NULL){ + logerror(nc, "Couldn't create the tab headers plane"); + ncplane_genocide(n); + free(nt); + return NULL; + } + }else{ + nopts.y = nopts.x = 0; + nopts.cols = ncols; + nopts.rows = 1; + if((nt->hp = ncplane_create(n, &nopts)) == NULL){ + logerror(nc, "Couldn't create the tab headers plane"); + ncplane_genocide(n); + free(nt); + return NULL; + } + nopts.y = 1; + nopts.rows = nrows - 1; + if((nt->p = ncplane_create(n, &nopts)) == NULL){ + logerror(nc, "Couldn't create the tab content plane"); + ncplane_genocide(n); + free(nt); + return NULL; + } + } + nctabbed_redraw(nt); + return nt; +} + +nctab* nctabbed_add(nctabbed* nt, nctab* after, nctab* before, tabcb cb, + const char* name, void* opaque){ + nctab* t; + notcurses* nc = ncplane_notcurses(nt->ncp); + if(after && before){ + if(after->prev != before || before->next != after){ + logerror(ncplane_notcurses(nt->ncp), "bad before (%p) / after (%p) spec\n", before, after); + return NULL; + } + }else if(!after && !before){ + // add it to the right of the selected tab + after = nt->selected; + } + if((t = malloc(sizeof(*t))) == NULL){ + logerror(nc, "Couldn't alocate nctab") + return NULL; + } + if((t->name = strdup(name)) == NULL){ + logerror(nc, "Couldn't allocate the tab name"); + free(t); + return NULL; + } + if((t->namecols = ncstrwidth(name)) < 0){ + logerror(nc, "Tab name contains illegal characters") + free(t->name); + free(t); + return NULL; + } + if(after){ + t->next = after->next; + t->prev = after; + after->next = t; + t->next->prev = t; + }else if(before){ + t->next = before; + t->prev = before->prev; + before->prev = t; + t->prev->next = t; + }else{ + // the first tab + t->prev = t->next = t; + nt->leftmost = nt->selected = t; + } + t->nt = nt; + t->cb = cb; + t->curry = opaque; + ++nt->tabcount; + return t; +} + +int nctabbed_del(nctabbed* nt, nctab* t){ + if(!t){ + logerror(ncplane_notcurses(nt->ncp), "Provided NULL nctab"); + return -1; + } + if(nt->tabcount == 1){ + nt->leftmost = nt->selected = NULL; + }else{ + if(nt->selected == t){ + nt->selected = t->next; + } + if(nt->leftmost == t){ + nt->leftmost = t->next; + } + t->next->prev = t->prev; + t->prev->next = t->next; + } + free(t); + --nt->tabcount; + return 0; +} + +int nctab_move(nctabbed* nt, nctab* t, nctab* after, nctab* before){ + if(after && before){ + if(after->prev != before || before->next != after){ + logerror(ncplane_notcurses(nt->ncp), "bad before (%p) / after (%p) spec\n", before, after); + return -1; + } + }else if(!after && !before){ + logerror(ncplane_notcurses(nt->ncp), "bad before (%p) / after (%p) spec\n", before, after); + return -1; + } + // bad things would happen + if(t == after || t == before){ + logerror(ncplane_notcurses(nt->ncp), "Cannot move a tab before or after itself."); + return -1; + } + t->prev->next = t->next; + t->next->prev = t->prev; + if(after){ + t->next = after->next; + t->prev = after; + after->next = t; + t->next->prev = t; + }else{ + t->next = before; + t->prev = before->prev; + before->prev = t; + t->prev->next = t; + } + return 0; +} + +void nctab_move_right(nctabbed* nt, nctab* t){ + if(t == nt->leftmost->prev){ + nctab_move(nt, t, NULL, nt->leftmost); + nt->leftmost = t; + return; + }else if(t == nt->leftmost){ + nt->leftmost = t->next; + } + nctab_move(nt, t, t->next, NULL); +} + +void nctab_move_left(nctabbed* nt, nctab* t){ + if(t == nt->leftmost){ + nt->leftmost = t->next; + nctab_move(nt, t, nt->leftmost->prev, NULL); + return; + }else if(t == nt->leftmost->next){ + nt->leftmost = t; + } + nctab_move(nt, t, NULL, t->prev); +} + +void nctabbed_rotate(nctabbed* nt, int amt){ + if(amt > 0){ + for(int i = 0 ; i < amt ; ++i){ + nt->leftmost = nt->leftmost->prev; + } + }else{ + for(int i = 0 ; i < -amt ; ++i){ + nt->leftmost = nt->leftmost->next; + } + } +} + +nctab* nctabbed_next(nctabbed* nt){ + if(nt->tabcount == 0){ + return NULL; + } + nt->selected = nt->selected->next; + return nt->selected; +} + +nctab* nctabbed_prev(nctabbed* nt){ + if(nt->tabcount == 0){ + return NULL; + } + nt->selected = nt->selected->prev; + return nt->selected; +} + +nctab* nctabbed_select(nctabbed* nt, nctab* t){ + nctab* prevsel = nt->selected; + nt->selected = t; + return prevsel; +} + +void nctabbed_channels(nctabbed* nt, uint64_t* RESTRICT hdrchan, + uint64_t* RESTRICT selchan, uint64_t* RESTRICT sepchan){ + memcpy(&nt->opts.hdrchan, hdrchan, sizeof(*hdrchan)); + memcpy(&nt->opts.selchan, selchan, sizeof(*selchan)); + memcpy(&nt->opts.sepchan, sepchan, sizeof(*sepchan)); +} + +const char* nctabbed_separator(nctabbed* nt){ + return nt->opts.separator; +} + +int nctabbed_separator_width(nctabbed* nt){ + return nt->sepcols; +} + +void nctabbed_destroy(nctabbed* nt){ + if(!nt){ + return; + } + nctab* t = nt->leftmost; + nctab* tmp; + t->prev->next = NULL; + while(t){ + tmp = t->next; + free(t->name); + free(t); + t = tmp; + } + ncplane_genocide(nt->ncp); + free(nt->opts.separator); + free(nt); +} + +void nctabbed_set_hdrchan(nctabbed* nt, uint64_t chan){ + nt->opts.hdrchan = chan; +} + +void nctabbed_set_selchan(nctabbed* nt, uint64_t chan){ + nt->opts.selchan = chan; +} + +void nctabbed_set_sepchan(nctabbed* nt, uint64_t chan){ + nt->opts.sepchan = chan; +} + +tabcb nctab_set_cb(nctab* t, tabcb newcb){ + tabcb prevcb = t->cb; + t->cb = newcb; + return prevcb; +} + +int nctab_set_name(nctab* t, const char* newname){ + int newnamecols; + char* prevname = t->name; + notcurses* nc = ncplane_notcurses(t->nt->ncp); + if((newnamecols = ncstrwidth(newname)) < 0){ + logerror(nc, "New tab name contains illegal characters"); + return -1; + } + if((t->name = strdup(newname)) == NULL){ + logerror(nc, "Couldn't allocate new tab name"); + t->name = prevname; + return -1; + } + free(prevname); + t->namecols = newnamecols; + return 0; +} + +void* nctab_set_userptr(nctab* t, void* newopaque){ + void* prevcurry = t->curry; + t->curry = newopaque; + return prevcurry; +} + +int nctabbed_set_separator(nctabbed* nt, const char* separator){ + int newsepcols; + char* prevsep = nt->opts.separator; + notcurses* nc = ncplane_notcurses(nt->ncp); + if((newsepcols = ncstrwidth(separator)) < 0){ + logerror(nc, "New tab separator contains illegal characters"); + return -1; + } + if((nt->opts.separator = strdup(separator)) == NULL){ + logerror(nc, "Couldn't allocate new tab separator"); + nt->opts.separator = prevsep; + return -1; + } + free(prevsep); + nt->sepcols = newsepcols; + return 0; +} diff --git a/src/poc/tabbed.c b/src/poc/tabbed.c new file mode 100644 index 000000000..e644bd807 --- /dev/null +++ b/src/poc/tabbed.c @@ -0,0 +1,143 @@ +#include + +#define REDRAW() \ + do{ \ + nctabbed_redraw(nct); \ + if(notcurses_render(nc) < 0){ \ + goto ded; \ + } \ + notcurses_getc_blocking(nc, NULL); \ + }while(0) + +void tabcbfn(struct nctab* t, struct ncplane* ncp, void* curry){ + (void) t; + (void) curry; + ncplane_erase(ncp); + ncplane_puttext(ncp, -1, NCALIGN_LEFT, + "Use left/right arrow keys for navigation, " + "'[' and ']' to rotate tabs, " + "'a' to add a tab, 'r' to remove a tab, " + "',' and '.' to move the selected tab, " + "and 'q' to quit", + NULL); +} + +void print_usage(char** argv){ + printf("Usage: %s [ -bht | --bottom | --help | --top ]...\n", argv[0]); +} + +int main(int argc, char** argv){ + struct notcurses* nc; + bool bottom = false; + for(int i = 1 ; i < argc ; ++i){ + if(strcmp(argv[i], "--help") == 0){ + print_usage(argv); + return EXIT_SUCCESS; + }else if(strcmp(argv[i], "--bottom") == 0){ + bottom = true; + }else if(strcmp(argv[i], "--top") == 0){ + bottom = false; + }else if(argv[i][0] == '-'){ + for(char* c = &argv[i][1] ; *c ; ++c){ + switch(*c){ + case 'h': + print_usage(argv); + return EXIT_SUCCESS; + case 'b': + bottom = true; + break; + case 't': + bottom = false; + break; + default: + print_usage(argv); + return EXIT_FAILURE; + } + } + }else{ + print_usage(argv); + return EXIT_FAILURE; + } + } + nc = notcurses_core_init(NULL, NULL); + if(!nc){ + return EXIT_FAILURE; + } + int rows, cols; + struct ncplane* stdp = notcurses_stddim_yx(nc, &rows, &cols); + struct ncplane_options popts = { + .y = 3, + .x = 5, + .rows = rows - 10, + .cols = cols - 10 + }; + struct ncplane* ncp = ncplane_create(stdp, &popts); + struct nctabbed_options topts = { + .hdrchan = CHANNELS_RGB_INITIALIZER(255, 0, 0, 60, 60, 60), + .selchan = CHANNELS_RGB_INITIALIZER(0, 255, 0, 0, 0, 0), + .sepchan = CHANNELS_RGB_INITIALIZER(255, 255, 255, 100, 100, 100), + .separator = " || ", + .flags = bottom ? NCTABBED_OPTION_BOTTOM : 0 + }; + struct nctabbed* nct = nctabbed_create(ncp, &topts); + struct nctab* t_; // stupid unused result warnings + (void) t_; + ncplane_set_base(nctabbed_content_plane(nct), " ", 0, CHANNELS_RGB_INITIALIZER(255, 255, 255, 15, 60, 15)); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "Tab #1", NULL); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "Tab #2", NULL); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "Tab #3", NULL); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "alpha", NULL); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "beta", NULL); + REDRAW(); + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "gamma", NULL); + REDRAW(); + char32_t c; + while((c = notcurses_getc_blocking(nc, NULL)) != 'q'){ + switch(c){ + case NCKEY_RIGHT: + nctabbed_next(nct); + break; + case NCKEY_LEFT: + nctabbed_prev(nct); + break; + case '[': + nctabbed_rotate(nct, -1); + break; + case ']': + nctabbed_rotate(nct, 1); + break; + case ',': + nctab_move_left(nct, nctabbed_selected(nct)); + break; + case '.': + nctab_move_right(nct, nctabbed_selected(nct)); + break; + case 'a': + t_ = nctabbed_add(nct, NULL, NULL, tabcbfn, "added tab", NULL); + break; + case 'r': + nctabbed_del(nct, nctabbed_selected(nct)); + break; + default:; + } + nctabbed_ensure_selected_header_visible(nct); + nctabbed_redraw(nct); + if(notcurses_render(nc)){ + goto ded; + } + } + goto fin; +ded: + notcurses_stop(nc); + return EXIT_FAILURE; +fin: + if(notcurses_stop(nc) < 0){ + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/src/tests/tabbed.cpp b/src/tests/tabbed.cpp new file mode 100644 index 000000000..f20ef82d6 --- /dev/null +++ b/src/tests/tabbed.cpp @@ -0,0 +1,394 @@ +#include "main.h" + +void tabbedcb(struct nctab*, struct ncplane*, void*){ + +} + +TEST_CASE("Tabbed") { + auto nc_ = testing_notcurses(); + if(!nc_){ + return; + } + struct ncplane* n_ = notcurses_stdplane(nc_); + REQUIRE(n_); + REQUIRE(0 == ncplane_cursor_move_yx(n_, 0, 0)); + + SUBCASE("CreateNullOpts") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + REQUIRE(nullptr != ncp); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + CHECK(0 == nctabbed_hdrchan(nt)); + CHECK(0 == nctabbed_selchan(nt)); + CHECK(0 == nctabbed_sepchan(nt)); + CHECK(nullptr == nctabbed_separator(nt)); + nctabbed_destroy(nt); + } + + SUBCASE("Create") { + struct nctabbed_options opts = { + .selchan = CHANNELS_RGB_INITIALIZER(0, 255, 0, 70, 70, 70), + .hdrchan = CHANNELS_RGB_INITIALIZER(255, 0, 0, 60, 60, 60), + .sepchan = CHANNELS_RGB_INITIALIZER(0, 0, 255, 60, 60, 60), + .separator = const_cast("-separator-"), + .flags = NCTABBED_OPTION_BOTTOM + }; + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, &opts); + REQUIRE(nullptr != nt); + CHECK(opts.hdrchan == nctabbed_hdrchan(nt)); + CHECK(opts.selchan == nctabbed_selchan(nt)); + CHECK(opts.sepchan == nctabbed_sepchan(nt)); + CHECK(0 == strcmp("-separator-", nctabbed_separator(nt))); + nctabbed_destroy(nt); + } + + SUBCASE("Add") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + CHECK(0 == nctabbed_tabcount(nt)); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "amazingtab123", nullptr); + CHECK(nullptr != t1); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(1 == nctabbed_tabcount(nt)); + CHECK(t1 == nctab_next(t1)); + CHECK(t1 == nctab_prev(t1)); + CHECK(nullptr == nctab_userptr(t1)); + CHECK(0 == strcmp("amazingtab123", nctab_name(t1))); + auto t2 = nctabbed_add(nt, nullptr, nullptr, nullptr, "nullcbtab", nullptr); + CHECK(nullptr != t2); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(2 == nctabbed_tabcount(nt)); + CHECK(t2 == nctab_next(t1)); + CHECK(t2 == nctab_prev(t1)); + CHECK(t1 == nctab_next(t2)); + CHECK(t1 == nctab_prev(t2)); + // this one gets put in the middle + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + CHECK(nullptr != t3); + CHECK(3 == nctabbed_tabcount(nt)); + CHECK(t2 == nctab_next(t3)); + CHECK(t1 == nctab_prev(t3)); + // this one at the end + auto t4 = nctabbed_add(nt, t2, t1, tabbedcb, "tab4", nullptr); + CHECK(nullptr != t4); + CHECK(4 == nctabbed_tabcount(nt)); + CHECK(t1 == nctab_next(t4)); + CHECK(t2 == nctab_prev(t4)); + // second to last + auto t5 = nctabbed_add(nt, nullptr, t4, tabbedcb, "tab5", nullptr); + CHECK(nullptr != t5); + CHECK(t4 == nctab_next(t5)); + CHECK(t2 == nctab_prev(t5)); + // second + auto t6 = nctabbed_add(nt, t1, nullptr, tabbedcb, "tab6", nullptr); + CHECK(nullptr != t6); + CHECK(t3 == nctab_next(t6)); + CHECK(t1 == nctab_prev(t6)); + nctabbed_destroy(nt); + } + + SUBCASE("Del") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + CHECK(5 == nctabbed_tabcount(nt)); + // tabs: t1 t2 t3 t4 t5 + CHECK(0 == nctabbed_del(nt, t5)); + CHECK(4 == nctabbed_tabcount(nt)); + CHECK(t1 == nctab_next(t4)); + CHECK(t4 == nctab_prev(t1)); + // tabs: t1 t2 t3 t4 + CHECK(0 == nctabbed_del(nt, t1)); + CHECK(3 == nctabbed_tabcount(nt)); + CHECK(t2 == nctab_next(t4)); + CHECK(t4 == nctab_prev(t2)); + CHECK(t2 == nctabbed_selected(nt)); + CHECK(t2 == nctabbed_leftmost(nt)); + // tabs: t2 t3 t4 + CHECK(0 == nctabbed_del(nt, t3)); + CHECK(2 == nctabbed_tabcount(nt)); + CHECK(t2 == nctab_next(t4)); + CHECK(t4 == nctab_prev(t2)); + // tabs: t2 t4 + CHECK(0 == nctabbed_del(nt, t2)); + CHECK(1 == nctabbed_tabcount(nt)); + CHECK(t4 == nctab_next(t4)); + CHECK(t4 == nctab_prev(t4)); + CHECK(t4 == nctabbed_selected(nt)); + CHECK(t4 == nctabbed_leftmost(nt)); + // only t4 left + CHECK(0 == nctabbed_del(nt, t4)); + CHECK(0 == nctabbed_tabcount(nt)); + CHECK(nullptr == nctabbed_selected(nt)); + CHECK(nullptr == nctabbed_leftmost(nt)); + nctabbed_destroy(nt); + } + + SUBCASE("Move") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + // tabs: t1 t2 t3 t4 t5 + nctab_move(nt, t4, t5, nullptr); + CHECK(t1 == nctab_next(t4)); + CHECK(t5 == nctab_prev(t4)); + CHECK(t2 == nctab_next(t1)); + CHECK(t4 == nctab_prev(t1)); + CHECK(t4 == nctab_next(t5)); + CHECK(t3 == nctab_prev(t5)); + // tabs: t1 t2 t3 t5 t4 + nctab_move(nt, t1, t2, nullptr); + CHECK(t3 == nctab_next(t1)); + CHECK(t2 == nctab_prev(t1)); + CHECK(t1 == nctab_next(t2)); + CHECK(t4 == nctab_prev(t2)); + CHECK(t5 == nctab_next(t3)); + CHECK(t1 == nctab_prev(t3)); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + // tabs: t1 t3 t5 t4 t2 + nctab_move(nt, t2, nullptr, t3); + CHECK(t2 == nctab_next(t1)); + CHECK(t4 == nctab_prev(t1)); + CHECK(t3 == nctab_next(t2)); + CHECK(t1 == nctab_prev(t2)); + CHECK(t5 == nctab_next(t3)); + CHECK(t2 == nctab_prev(t3)); + // tabs: t1 t2 t3 t5 t4 + nctab_move(nt, t1, nullptr, t4); + CHECK(t1 == nctab_next(t5)); + CHECK(t3 == nctab_prev(t5)); + CHECK(t4 == nctab_next(t1)); + CHECK(t5 == nctab_prev(t1)); + CHECK(t2 == nctab_next(t4)); + CHECK(t1 == nctab_prev(t4)); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + // tabs: t1 t4 t2 t3 t5 + nctabbed_destroy(nt); + } + + SUBCASE("Rotate") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + // tabs: t1 t2 t3 t4 t5 + nctabbed_rotate(nt, 1); + CHECK(t5 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t5 t1 t2 t3 t4 + nctabbed_rotate(nt, 7); + CHECK(t3 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t3 t4 t5 t1 t2 + nctabbed_rotate(nt, -1); + CHECK(t4 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t4 t5 t1 t2 t3 + nctabbed_rotate(nt, -13); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t3 t4 t5 t1 + nctabbed_destroy(nt); + } + + SUBCASE("MoveLeftRight") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + // tabs: t1 t2 t3 t4 t5 + nctab_move_right(nt, t1); + CHECK(t3 == nctab_next(t1)); + CHECK(t2 == nctab_prev(t1)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t1 t3 t4 t5 + nctab_move_right(nt, t1); + CHECK(t4 == nctab_next(t1)); + CHECK(t3 == nctab_prev(t1)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t3 t1 t4 t5 + nctab_move_right(nt, t5); + CHECK(t2 == nctab_next(t5)); + CHECK(t4 == nctab_prev(t5)); + CHECK(t5 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t5 t2 t3 t1 t4 + nctab_move_right(nt, t5); + CHECK(t3 == nctab_next(t5)); + CHECK(t2 == nctab_prev(t5)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t5 t3 t1 t4 + nctab_move_left(nt, t5); + CHECK(t2 == nctab_next(t5)); + CHECK(t4 == nctab_prev(t5)); + CHECK(t5 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t5 t2 t3 t1 t4 + nctab_move_left(nt, t5); + CHECK(t2 == nctab_next(t5)); + CHECK(t4 == nctab_prev(t5)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t3 t1 t4 t5 + nctab_move_left(nt, t5); + CHECK(t4 == nctab_next(t5)); + CHECK(t1 == nctab_prev(t5)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t3 t1 t5 t4 + nctab_move_left(nt, t5); + CHECK(t1 == nctab_next(t5)); + CHECK(t3 == nctab_prev(t5)); + CHECK(t2 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_selected(nt)); + // tabs: t2 t3 t5 t1 t4 + nctabbed_destroy(nt); + } + + SUBCASE("Select") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + (void)t3; + (void)t4; + CHECK(t1 == nctabbed_selected(nt)); + nctabbed_select(nt, t2); + CHECK(t2 == nctabbed_selected(nt)); + nctabbed_select(nt, t5); + CHECK(t5 == nctabbed_selected(nt)); + nctabbed_select(nt, t5); + CHECK(t5 == nctabbed_selected(nt)); + nctabbed_select(nt, t1); + CHECK(t1 == nctabbed_selected(nt)); + nctabbed_destroy(nt); + } + + SUBCASE("NextPrev") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + auto t5 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab5", nullptr); + auto t4 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab4", nullptr); + auto t3 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab3", nullptr); + auto t2 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab2", nullptr); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t2 == nctabbed_next(nt)); + CHECK(t2 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(t3 == nctabbed_next(nt)); + CHECK(t3 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(t4 == nctabbed_next(nt)); + CHECK(t4 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(t5 == nctabbed_next(nt)); + CHECK(t5 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + CHECK(t1 == nctabbed_next(nt)); + CHECK(t1 == nctabbed_selected(nt)); + CHECK(t1 == nctabbed_leftmost(nt)); + nctabbed_destroy(nt); + } + + SUBCASE("Setters") { + struct ncplane_options nopts = { + .y = 1, .x = 2, .rows = ncplane_dim_y(n_) - 2, .cols = ncplane_dim_x(n_) - 4, + .userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0 + }; + auto ncp = ncplane_create(n_, &nopts); + auto nt = nctabbed_create(ncp, nullptr); + REQUIRE(nullptr != nt); + uint64_t hdrchan = CHANNELS_RGB_INITIALIZER(255, 127, 63, 31, 15, 7); + uint64_t selchan = CHANNELS_RGB_INITIALIZER(127, 63, 31, 15, 7, 3); + uint64_t sepchan = CHANNELS_RGB_INITIALIZER(63, 31, 15, 7, 3, 1); + nctabbed_set_hdrchan(nt, hdrchan); + nctabbed_set_selchan(nt, selchan); + nctabbed_set_sepchan(nt, sepchan); + uint64_t hdrchan2, selchan2, sepchan2; + nctabbed_channels(nt, &hdrchan2, &selchan2, &sepchan2); + CHECK(hdrchan == hdrchan2); + CHECK(selchan == selchan2); + CHECK(sepchan == sepchan2); + const char* sep = "separateur"; + nctabbed_set_separator(nt, sep); + CHECK(0 == strcmp(sep, nctabbed_separator(nt))); + auto t1 = nctabbed_add(nt, nullptr, nullptr, tabbedcb, "tab1", nullptr); + nctab_set_cb(t1, tabbedcb); + CHECK(tabbedcb == nctab_cb(t1)); + nctab_set_userptr(t1, (void*) sep); + CHECK((void*) sep == nctab_userptr(t1)); + const char* tname = "tab name"; + nctab_set_name(t1, tname); + CHECK(0 == strcmp(tname, nctab_name(t1))); + nctabbed_destroy(nt); + } +}