xF: add a scrollbar to the buffer list

And clean this area up.
This commit is contained in:
2026-08-02 22:45:28 +02:00
parent f8ecf7d0f7
commit 8f3df19786
+228 -106
View File
@@ -267,6 +267,8 @@ enum action
ACTION_FORMAT_UNDERLINE,
ACTION_SCROLL_UP,
ACTION_SCROLL_DOWN,
ACTION_BUFFER_LIST_SCROLL_UP,
ACTION_BUFFER_LIST_SCROLL_DOWN,
ACTION_SUBMIT,
ACTION_COMPLETE,
ACTION_HISTORY_PREVIOUS,
@@ -357,8 +359,10 @@ enum app_widget_id
WIDGET_NONE,
WIDGET_BUTTON,
WIDGET_BUFFER,
WIDGET_BUFFER_LIST,
WIDGET_TRANSCRIPT,
WIDGET_SCROLLBAR,
WIDGET_BUFFER_LIST_SCROLLBAR,
WIDGET_TRANSCRIPT_SCROLLBAR,
};
struct app_backend
@@ -452,7 +456,8 @@ static struct
struct attrs attrs[ATTRIBUTE_COUNT];
struct line_editor editor; ///< Message editor
int64_t last_active; ///< Last ACTIVE command timestamp
bool dragging_scrollbar; ///< Mouse drag updates the viewport
int buffer_list_offset; ///< XUI pixels from the top of the list
enum app_widget_id dragged_scrollbar;
struct binding *keys; ///< Parsed xC-style key bindings
size_t keys_len;
@@ -882,6 +887,7 @@ app_reset_relay_state (void)
{
g.buffer_current = NULL;
g.buffer_last = NULL;
g.buffer_list_offset = 0;
LIST_FOR_EACH (struct buffer, iter, g.buffers)
buffer_destroy (iter);
@@ -940,6 +946,8 @@ relay_process_response (const struct relay_event_data_response *e)
// ~~~ State synchronization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static void app_buffer_list_ensure_visible (const struct buffer *buffer);
static struct buffer *
buffer_find (const char *name)
{
@@ -1143,6 +1151,7 @@ relay_process_buffer_activate (struct buffer *b)
if (b)
{
b->scroll_offset = 0;
app_buffer_list_ensure_visible (b);
app_editor_set (b->input);
g.editor.point = MIN (b->input_point, (int) g.editor.len);
app_recheck_current_highlight ();
@@ -1560,6 +1569,23 @@ relay_reconnect_now (void)
// --- Viewport policy ---------------------------------------------------------
static int
app_viewport_height (void)
{
return MAX (0, g_xui.height - 3 * g_xui.vunit);
}
static int
app_buffer_list_content_height (void)
{
size_t count = 0;
LIST_FOR_EACH (struct buffer, buffer, g.buffers)
count++;
uint64_t height = count * (uint64_t) g_xui.vunit;
return MIN ((uint64_t) INT_MAX, height);
}
static size_t
app_visible_line_count (const struct buffer *b)
{
@@ -1570,37 +1596,6 @@ app_visible_line_count (const struct buffer *b)
return count;
}
static const struct buffer_line *
app_unread_marker_line (const struct buffer *b)
{
if (!buffer_has_unread_marker (b))
return NULL;
uint64_t unread = (uint64_t) b->new_messages + b->new_unimportant_messages;
const struct buffer_line *marker = b->lines_tail;
while (unread > 1 && marker->prev)
{
unread--;
marker = marker->prev;
}
return marker;
}
static int
app_viewport_height (void)
{
return MAX (0, g_xui.height - 3 * g_xui.vunit);
}
static int
app_buffer_content_height (const struct buffer *b)
{
uint64_t height = app_visible_line_count (b) * (uint64_t) g_xui.vunit;
if (buffer_has_unread_marker (b))
height += UNREAD_MARKER_HEIGHT;
return MIN ((uint64_t) INT_MAX, height);
}
static int
app_content_height (void)
{
@@ -1609,7 +1604,15 @@ app_content_height (void)
return MIN ((uint64_t) INT_MAX,
g.log.lines.len * (uint64_t) g_xui.vunit);
}
return g.buffer_current ? app_buffer_content_height (g.buffer_current) : 0;
if (g.buffer_current)
{
struct buffer *b = g.buffer_current;
uint64_t height = app_visible_line_count (b) * (uint64_t) g_xui.vunit;
if (buffer_has_unread_marker (b))
height += UNREAD_MARKER_HEIGHT;
return MIN ((uint64_t) INT_MAX, height);
}
return 0;
}
static int *
@@ -1617,36 +1620,102 @@ app_content_scroll_offset (void)
{
if (g.log.visible)
return &g.log.offset;
return g.buffer_current ? &g.buffer_current->scroll_offset : NULL;
if (g.buffer_current)
return &g.buffer_current->scroll_offset;
return NULL;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct app_scroll_view
{
int *offset; ///< Pixels from the anchored edge
int total; ///< Total content height in pixels
bool from_bottom; ///< The offset is bottom-anchored
};
static struct app_scroll_view
app_scroll_view (enum app_widget_id id)
{
if (id == WIDGET_BUFFER_LIST_SCROLLBAR)
return (struct app_scroll_view) { &g.buffer_list_offset,
app_buffer_list_content_height (), false };
hard_assert (id == WIDGET_TRANSCRIPT_SCROLLBAR);
return (struct app_scroll_view) { app_content_scroll_offset (),
app_content_height (), true };
}
static int
app_content_top (void)
app_scroll_view_origin (struct app_scroll_view *self)
{
int total = app_content_height ();
int *offset = app_content_scroll_offset ();
return MAX (0, total - app_viewport_height () - (offset ? *offset : 0));
if (!self->offset)
return 0;
int maximum = MAX (0, self->total - app_viewport_height ());
*self->offset = MAX (0, MIN (maximum, *self->offset));
return self->from_bottom
? self->total - app_viewport_height () - *self->offset
: *self->offset;
}
static bool
app_scroll (int amount)
app_scroll_view_set_origin (struct app_scroll_view *self, int64_t origin)
{
int *offset = app_content_scroll_offset ();
if (!offset)
if (!self->offset)
return false;
int maximum = MAX (0, app_content_height () - app_viewport_height ());
int64_t requested = (int64_t) *offset + amount;
int updated = MAX (0, MIN ((int64_t) maximum, requested));
if (updated == *offset)
int maximum = MAX (0, self->total - app_viewport_height ());
int updated = MAX (0, MIN ((int64_t) maximum, origin));
if (self->from_bottom)
updated = maximum - updated;
if (updated == *self->offset)
return false;
*offset = updated;
app_recheck_current_highlight ();
*self->offset = updated;
return true;
}
static bool
app_scroll (enum app_widget_id id, int amount)
{
struct app_scroll_view view = app_scroll_view (id);
if (!app_scroll_view_set_origin (&view,
(int64_t) app_scroll_view_origin (&view) + amount))
return false;
if (view.from_bottom)
app_recheck_current_highlight ();
xui_invalidate ();
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
app_buffer_list_ensure_visible (const struct buffer *buffer)
{
int visible = app_viewport_height ();
if (visible <= 0 || g_xui.vunit <= 0)
return;
int64_t row_top = 0;
struct buffer *iter;
for (iter = g.buffers; iter && iter != buffer; iter = iter->next)
row_top += g_xui.vunit;
if (!iter)
return;
struct app_scroll_view view =
app_scroll_view (WIDGET_BUFFER_LIST_SCROLLBAR);
int64_t origin = app_scroll_view_origin (&view);
if (row_top < origin)
origin = row_top;
else if (row_top + g_xui.vunit > origin + visible)
origin = row_top + g_xui.vunit - visible;
app_scroll_view_set_origin (&view, origin);
}
// --- Terminal widgets --------------------------------------------------------
static void
@@ -1835,6 +1904,25 @@ app_select_backend (void)
#endif // WITH_X11
}
static struct widget *
app_make_scrollbar (enum app_widget_id id)
{
struct app_scroll_view view = app_scroll_view (id);
int top = MAX (0, app_scroll_view_origin (&view));
if (app_viewport_height () <= 0
|| view.total <= app_viewport_height ())
return NULL;
struct widget *scrollbar = g_xui.ui->scrollbar
(g_attrs[ATTRIBUTE_SCROLLBAR].attrs, top, view.total);
#ifdef WITH_X11
if (g_xui.ui == &x11_ui)
scrollbar->on_render = app_x11_render_scrollbar;
#endif // WITH_X11
scrollbar->widget_id = id;
return scrollbar;
}
// --- Layout and rendering ----------------------------------------------------
struct layout
@@ -2059,18 +2147,53 @@ app_make_buffer_row (struct buffer *b, int index)
return widget;
}
static void
app_on_viewport_allocated (struct widget *self)
{
xui_on_vbox_allocated (self);
int y = self->y + self->userdata;
LIST_FOR_EACH (struct widget, child, self->children)
{
widget_move (child, self->x - child->x, y - child->y);
y += child->height;
}
}
static struct widget *
app_make_buffer_list (void)
{
struct layout rows = {};
int index = 0;
struct app_scroll_view view =
app_scroll_view (WIDGET_BUFFER_LIST_SCROLLBAR);
int top = app_scroll_view_origin (&view);
int bottom = top + app_viewport_height ();
int index = 0, first = top / g_xui.vunit;
LIST_FOR_EACH (struct buffer, buffer, g.buffers)
app_push (&rows, app_make_buffer_row (buffer, index++));
{
int row_top = index * g_xui.vunit;
if (index >= first && row_top < bottom)
app_push (&rows, app_make_buffer_row (buffer, index));
index++;
}
struct widget *list = xui_vbox (rows.head);
list->width = MIN (20 * g_xui.hunit, g_xui.width / 3);
list->height = -1;
return list;
list->on_allocated = app_on_viewport_allocated;
list->userdata = -(top % g_xui.vunit);
list->width = list->height = -1;
struct layout viewport = {};
app_push (&viewport, list);
struct widget *scrollbar =
app_make_scrollbar (WIDGET_BUFFER_LIST_SCROLLBAR);
if (scrollbar)
app_push (&viewport, scrollbar);
struct widget *viewport_box = xui_hbox (viewport.head);
viewport_box->widget_id = WIDGET_BUFFER_LIST;
viewport_box->width = MIN (20 * g_xui.hunit, g_xui.width / 3);
viewport_box->height = -1;
return viewport_box;
}
static enum app_attribute
@@ -2200,20 +2323,15 @@ struct transcript_slice
static struct transcript_slice
app_make_transcript_slice (void)
{
int *scroll_offset = app_content_scroll_offset ();
hard_assert (scroll_offset);
int total = app_content_height ();
struct app_scroll_view view =
app_scroll_view (WIDGET_TRANSCRIPT_SCROLLBAR);
int viewport = app_viewport_height ();
int maximum = MAX (0, total - viewport);
*scroll_offset = MAX (0, MIN (*scroll_offset, maximum));
int view_top = total - viewport - *scroll_offset;
int view_top = app_scroll_view_origin (&view);
return (struct transcript_slice)
{
.view_top = view_top,
.clip_top = MAX (0, view_top),
.clip_bottom = MIN (total, view_top + viewport),
.clip_bottom = MIN (view.total, view_top + viewport),
.first_y = -1,
};
}
@@ -2251,6 +2369,22 @@ app_append_log_rows (struct layout *rows)
return app_transcript_slice_offset (&slice);
}
static const struct buffer_line *
app_unread_marker_line (const struct buffer *b)
{
if (!buffer_has_unread_marker (b))
return NULL;
uint64_t unread = (uint64_t) b->new_messages + b->new_unimportant_messages;
const struct buffer_line *marker = b->lines_tail;
while (unread > 1 && marker->prev)
{
unread--;
marker = marker->prev;
}
return marker;
}
static int
app_append_buffer_rows (struct layout *rows)
{
@@ -2274,19 +2408,6 @@ app_append_buffer_rows (struct layout *rows)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
app_on_transcript_allocated (struct widget *self)
{
xui_on_vbox_allocated (self);
int y = self->y + self->userdata;
LIST_FOR_EACH (struct widget, child, self->children)
{
widget_move (child, self->x - child->x, y - child->y);
y += child->height;
}
}
static struct widget *
app_make_transcript (void)
{
@@ -2296,7 +2417,7 @@ app_make_transcript (void)
: app_append_buffer_rows (&rows);
struct widget *transcript = xui_vbox (rows.head);
transcript->on_allocated = app_on_transcript_allocated;
transcript->on_allocated = app_on_viewport_allocated;
transcript->widget_id = WIDGET_TRANSCRIPT;
transcript->userdata = offset;
transcript->width = transcript->height = -1;
@@ -2311,19 +2432,9 @@ app_make_middle (void)
struct layout viewport = {};
app_push (&viewport, app_make_transcript ());
int viewport_height = app_viewport_height ();
if (viewport_height > 0 && app_content_height () > viewport_height)
{
struct widget *scrollbar = g_xui.ui->scrollbar
(g_attrs[ATTRIBUTE_SCROLLBAR].attrs,
app_content_top (), app_content_height ());
#ifdef WITH_X11
if (g_xui.ui == &x11_ui)
scrollbar->on_render = app_x11_render_scrollbar;
#endif // WITH_X11
scrollbar->widget_id = WIDGET_SCROLLBAR;
struct widget *scrollbar = app_make_scrollbar (WIDGET_TRANSCRIPT_SCROLLBAR);
if (scrollbar)
app_push (&viewport, scrollbar);
}
struct widget *viewport_box = xui_hbox (viewport.head);
viewport_box->width = viewport_box->height = -1;
@@ -2594,19 +2705,15 @@ app_scrollbar_seek (struct widget *scrollbar, int y)
if (!scrollbar || scrollbar->height <= 0)
return false;
int total = app_content_height ();
int visible = app_viewport_height ();
int maximum = MAX (0, total - visible);
struct app_scroll_view view = app_scroll_view (scrollbar->widget_id);
int top = (double) MAX (0, MIN (scrollbar->height, y))
/ scrollbar->height * total - visible / 2;
top = MAX (0, MIN (maximum, top));
int *offset = app_content_scroll_offset ();
if (!offset)
/ scrollbar->height * view.total - app_viewport_height () / 2;
if (!view.offset)
return false;
*offset = maximum - top;
app_recheck_current_highlight ();
app_scroll_view_set_origin (&view, top);
if (view.from_bottom)
app_recheck_current_highlight ();
xui_invalidate ();
return true;
}
@@ -2844,9 +2951,13 @@ app_process_action (enum action action)
app_editor_insert (0x1f);
return true;
case ACTION_SCROLL_UP:
return app_scroll (+page);
return app_scroll (WIDGET_TRANSCRIPT_SCROLLBAR, -page);
case ACTION_SCROLL_DOWN:
return app_scroll (-page);
return app_scroll (WIDGET_TRANSCRIPT_SCROLLBAR, +page);
case ACTION_BUFFER_LIST_SCROLL_UP:
return app_scroll (WIDGET_BUFFER_LIST_SCROLLBAR, -page);
case ACTION_BUFFER_LIST_SCROLL_DOWN:
return app_scroll (WIDGET_BUFFER_LIST_SCROLLBAR, +page);
case ACTION_SUBMIT:
app_submit_input ();
return true;
@@ -2875,15 +2986,16 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
app_notify_active ();
if (type == TERMO_MOUSE_RELEASE)
{
g.dragging_scrollbar = false;
g.dragged_scrollbar = WIDGET_NONE;
return true;
}
if (type == TERMO_MOUSE_DRAG)
{
if (!g.dragging_scrollbar)
if (!g.dragged_scrollbar)
return true;
struct widget *scrollbar =
app_widget_by_id (g_xui.widgets, WIDGET_SCROLLBAR);
app_widget_by_id (g_xui.widgets, g.dragged_scrollbar);
return app_scrollbar_seek (scrollbar,
scrollbar ? y - scrollbar->y : 0);
}
@@ -2892,18 +3004,26 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
if (button == 4 || button == 5)
{
int amount = (button == 4 ? -1 : 1) * 3 * g_xui.vunit;
struct widget *buffer_list =
app_widget_by_id (g_xui.widgets, WIDGET_BUFFER_LIST);
if (buffer_list && app_widget_contains (buffer_list, x, y))
return app_scroll (WIDGET_BUFFER_LIST_SCROLLBAR, amount);
struct widget *transcript =
app_widget_by_id (g_xui.widgets, WIDGET_TRANSCRIPT);
return transcript && app_widget_contains (transcript, x, y)
&& app_scroll ((button == 4 ? 1 : -1) * 3 * g_xui.vunit);
&& app_scroll (WIDGET_TRANSCRIPT_SCROLLBAR, amount);
}
if (button != 1)
return false;
struct widget *target = app_interactive_widget_at (g_xui.widgets, x, y);
if (target && target->widget_id == WIDGET_SCROLLBAR)
if (target && (target->widget_id == WIDGET_BUFFER_LIST_SCROLLBAR
|| target->widget_id == WIDGET_TRANSCRIPT_SCROLLBAR))
{
g.dragging_scrollbar = true;
g.dragged_scrollbar = target->widget_id;
return app_scrollbar_seek (target, y - target->y);
}
if (target && target->widget_id == WIDGET_BUTTON)
@@ -2951,6 +3071,8 @@ g_default_bindings[] =
{ "M-m", ACTION_FORMAT },
{ "PageUp", ACTION_SCROLL_UP },
{ "PageDown", ACTION_SCROLL_DOWN },
{ "S-PageUp", ACTION_BUFFER_LIST_SCROLL_UP },
{ "S-PageDown", ACTION_BUFFER_LIST_SCROLL_DOWN },
{ "Enter", ACTION_SUBMIT },
{ "C-j", ACTION_SUBMIT },
{ "Tab", ACTION_COMPLETE },