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);
+ }
+}