From 92978a10576d52a0f6c9983d3b6afae7c40eff40 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Thu, 12 Mar 2026 15:29:23 -0700 Subject: Support scrolling by dragging scrollbars --- src/constants.h | 2 +- src/input.c | 226 ++++++++++++++++++++++++++++++++++++++++++------- src/layout.c | 21 ++++- src/render.c | 13 ++- src/uiLibrary.h | 23 +++++ src/widget/scrollbar.c | 8 ++ src/widget/table.c | 58 +++++++++---- src/widget/widget.c | 21 ++++- src/widget/widget.h | 49 +++++++---- 9 files changed, 340 insertions(+), 81 deletions(-) create mode 100644 src/widget/scrollbar.c (limited to 'src') diff --git a/src/constants.h b/src/constants.h index 0d93d14..47babab 100644 --- a/src/constants.h +++ b/src/constants.h @@ -4,4 +4,4 @@ #define MaxWidgetEvents 8 // Width of scroll bars in pixels. -#define ScrollBarWidth 32 +#define ScrollbarWidth 32 diff --git a/src/input.c b/src/input.c index 4461c8b..6a7f4d0 100644 --- a/src/input.c +++ b/src/input.c @@ -6,9 +6,6 @@ #include -#define Min(a, b) ((a) < (b) ? (a) : (b)) -#define Max(a, b) ((a) > (b) ? (a) : (b)) - /// Return true if the rectangle contains the point. static bool RectContains(uiRect rect, uiPoint point) { return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) && @@ -35,17 +32,59 @@ static uiWidget* GetWidgetUnderMouse(uiWidget* parent, uiPoint mouse) { return 0; } -/// Get the table row at the given pixel position. +// ----------------------------------------------------------------------------- +// Scrollbar. + +/// Process a scrollbar mouse button event. +static void MouseButtonScrollbar( + uiScrollbar* scrollbar, const uiMouseButtonEvent* event) { + assert(scrollbar); + assert(event); + + if (event->button_state == uiMouseDown) { + UI_LOG("scroll start"); + // TODO: I don't quite like this global state, but I also don't want to + // store input state inside the widgets. Define a struct for input handling + // and pass it to these functions instead? + g_ui.scroll.scrollbar_handle_y_start = scrollbar->handle_y; + g_ui.scroll.scrolling = true; + } else if (event->button_state == uiMouseUp) { + UI_LOG("scroll end"); + g_ui.scroll.scrolling = false; + } +} + +/// Process a scrollbar mouse move event. +/// +/// Return the amount scrolled relative to the scroll starting position, in +/// pixels. +static int MouseMoveScrollbar( + uiScrollbar* scrollbar, const uiMouseMoveEvent* event) { + assert(scrollbar); + assert(event); + + const int delta = event->mouse_position.y - g_ui.mouse_down.start_point.y; + ScrollbarScroll(scrollbar, g_ui.scroll.scrollbar_handle_y_start + delta); + return delta; +} + +// ----------------------------------------------------------------------------- +// Table. + +/// Get the table row and column at the given pixel position, or whether the +/// scrollbar was hit. static void GetTableRowColAtXy( - const uiTable* table, uiPoint p, int* out_row, int* out_col) { + const uiTable* table, uiPoint p, int* out_row, int* out_col, + bool* out_scrollbar) { assert(table); assert(out_row); assert(out_col); const uiWidget* widget = (uiWidget*)table; - int col = -1; - int row = -1; + int col = -1; + int row = -1; + bool scrollbar = false; if (RectContains(widget->rect, p)) { int x = p.x - widget->rect.x; @@ -55,14 +94,40 @@ static void GetTableRowColAtXy( // 0 is the header, and we want to map the first row to 0, so -1. row = table->offset + ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1; - // Out-of-bounds check. - if ((col >= table->cols) || (row >= table->rows)) { + // Scrollbar area check. + if ((col >= table->cols) && (row < table->rows)) { + col = -1; + scrollbar = true; + } + // Out of bounds. + else if ((col >= table->cols) || (row >= table->rows)) { col = row = -1; } } - *out_col = col; - *out_row = row; + *out_col = col; + *out_row = row; + *out_scrollbar = scrollbar; +} + +/// Process a table mouse button event. +static void MouseButtonTable(uiTable* table, const uiMouseButtonEvent* event) { + assert(table); + assert(event); + + if (event->button_state == uiMouseDown) { + int row, col; + bool scrollbar; + GetTableRowColAtXy(table, event->mouse_position, &row, &col, &scrollbar); + + if (scrollbar) { + MouseButtonScrollbar(&table->scrollbar, event); + } + } else if ((event->button_state == uiMouseUp) && g_ui.scroll.scrolling) { + // The mouse up event need not happen while the mouse is on the scrollbar, + // so process mouse-up regardless of whether the mouse is. + MouseButtonScrollbar(&table->scrollbar, event); + } } /// Process a table click event. @@ -70,8 +135,9 @@ static void ClickTable(uiTable* table, const uiMouseClickEvent* event) { assert(table); assert(event); - int row, col; - GetTableRowColAtXy(table, event->mouse_position, &row, &col); + int row, col; + bool scrollbar; + GetTableRowColAtXy(table, event->mouse_position, &row, &col, &scrollbar); if ((row != -1) && (col != -1)) { PushWidgetEvent(&(uiWidgetEvent){ @@ -86,14 +152,25 @@ static void ClickTable(uiTable* table, const uiMouseClickEvent* event) { static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) { assert(table); assert(event); - table->offset = - Min(table->rows - table->num_visible_rows, - Max(0, table->offset - event->scroll_offset)); + + const int row = table->offset - event->scroll_offset; + uiTableScroll(table, row); } -/// Process a scroll event. -static bool ProcessScrollEvent( - uiWidget* widget, const uiMouseScrollEvent* event) { +/// Process a table mouse move event. +static void MouseMoveTable(uiTable* table, const uiMouseMoveEvent* event) { + assert(table); + assert(event); + + if (g_ui.scroll.scrolling) { + MouseMoveScrollbar(&table->scrollbar, event); + SyncTableToScrollbar(table); + } +} + +/// Process a mouse button event. +static bool ProcessMouseButtonEvent( + uiWidget* widget, const uiMouseButtonEvent* event) { assert(widget); assert(event); @@ -101,7 +178,7 @@ static bool ProcessScrollEvent( switch (widget->type) { case uiTypeTable: - ScrollTable((uiTable*)widget, event); + MouseButtonTable((uiTable*)widget, event); processed = true; break; default: @@ -112,7 +189,7 @@ static bool ProcessScrollEvent( } /// Process a click event. -static bool ProcessClickEvent( +static bool ProcessMouseClickEvent( uiWidget* widget, const uiMouseClickEvent* event) { assert(widget); assert(event); @@ -131,21 +208,89 @@ static bool ProcessClickEvent( return processed; } +/// Process a scroll event. +static bool ProcessMouseScrollEvent( + uiWidget* widget, const uiMouseScrollEvent* event) { + assert(widget); + assert(event); + + bool processed = false; + + switch (widget->type) { + case uiTypeTable: + ScrollTable((uiTable*)widget, event); + processed = true; + break; + default: + break; + } + + return processed; +} + +/// Process a mouse move event. +static bool ProcessMouseMoveEvent( + uiWidget* widget, const uiMouseMoveEvent* event) { + assert(widget); + assert(event); + + bool processed = false; + + switch (widget->type) { + case uiTypeTable: + MouseMoveTable((uiTable*)widget, event); + processed = true; + break; + default: + break; + } + + return processed; +} + bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) { assert(frame); assert(event); - uiWidget* widget = (uiWidget*)frame; - + // TODO: processed != redraw. The client will redraw if this function + // returns + // true, but whether an event was processed does it imply that the UI needs + // a redraw. + // + // TODO: Also think about limiting redraws in xplorer to like 25fps or + // something in a "battery saving" mode of sorts. bool processed = false; switch (event->type) { case uiEventMouseButton: { const uiMouseButtonEvent* ev = &event->mouse_button; - uiMouseButtonState* prev_state = &g_ui.mouse_button_state[ev->button]; + // Update the mouse button state. + uiMouseButtonState* button_state = &g_ui.mouse_button_state[ev->button]; + const uiMouseButtonState prev_state = *button_state; + *button_state = ev->button_state; + + // If this is a mouse up event and a widget is currently being + // mouse-downed, send the event to that widget. + if ((ev->button_state == uiMouseUp) && + !uiIsNullptr(g_ui.mouse_down.widget)) { + processed = ProcessMouseButtonEvent(g_ui.mouse_down.widget.widget, ev); + g_ui.mouse_down = (uiMouseDownState){0}; + } else { // Mouse down or no widget. + uiWidget* target = + GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position); + if (target) { + processed = ProcessMouseButtonEvent(target, ev); + if (processed && (ev->button_state == uiMouseDown)) { + g_ui.mouse_down.widget = uiMakeWidgetPtr(target); + g_ui.mouse_down.start_point = ev->mouse_position; + } else { + g_ui.mouse_down = (uiMouseDownState){0}; + } + } + } - if ((*prev_state == uiMouseDown) && (ev->state == uiMouseUp)) { + if ((prev_state == uiMouseDown) && (ev->button_state == uiMouseUp)) { // Click. uiSendEvent( frame, @@ -155,26 +300,45 @@ bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) { .button = ev->button, .mouse_position = ev->mouse_position} }); } - - *prev_state = ev->state; break; } case uiEventMouseClick: { const uiMouseClickEvent* ev = &event->mouse_click; - uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); + + uiWidget* target = + GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position); if (target) { - processed = ProcessClickEvent(target, ev); + processed = ProcessMouseClickEvent(target, ev); } break; } case uiEventMouseScroll: { const uiMouseScrollEvent* ev = &event->mouse_scroll; - uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); + + uiWidget* target = + GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position); if (target) { - processed = ProcessScrollEvent(target, ev); + processed = ProcessMouseScrollEvent(target, ev); } break; } + case uiEventMouseMove: { + const uiMouseMoveEvent* ev = &event->mouse_move; + + // If a widget is currently being moused-down, send the event to that + // widget. + if (!uiIsNullptr(g_ui.mouse_down.widget)) { + processed = ProcessMouseMoveEvent(g_ui.mouse_down.widget.widget, ev); + } else { + uiWidget* target = + GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position); + if (target) { + processed = ProcessMouseMoveEvent(target, ev); + } + } + + break; + } } return processed; diff --git a/src/layout.c b/src/layout.c index c8f5995..5261eb3 100644 --- a/src/layout.c +++ b/src/layout.c @@ -51,7 +51,7 @@ static void ResizeTable(uiTable* table, int width, int height) { // Table contents. for (int row = 0; row < table->rows; ++row) { for (int col = 0; col < table->cols; ++col) { - const uiCell* cell = GetCell(table, row, col); + const uiCell* cell = TableGetCell(table, row, col); const int length = (int)string_length(cell->text); widths[col] = length > widths[col] ? length : widths[col]; @@ -110,13 +110,28 @@ static void ResizeTable(uiTable* table, int width, int height) { } // Now make room for the scroll bar, if necessary. + uiScrollbar* scrollbar = &table->scrollbar; if (table->flags.vertical_overflow) { - const int offset = ScrollBarWidth / table->cols; - const int remainder = ScrollBarWidth % table->cols; + // Subtract room from table columns. + const int offset = ScrollbarWidth / table->cols; + const int remainder = ScrollbarWidth % table->cols; for (int col = 0; col < table->cols; ++col) { table->widths[col] -= offset + (col < remainder ? 1 : 0); assert(table->widths[col] >= 0); } + + // Set scrollbar layout. + scrollbar->width = ScrollbarWidth; + scrollbar->height = table->height; + scrollbar->handle_height = + (int)((double)table->num_visible_rows / (double)table->rows * + (double)table->height); + uiTableScroll(table, table->offset); + } else { // Scroll bar not visible. + scrollbar->width = 0; + scrollbar->height = 0; + scrollbar->handle_height = 0; + scrollbar->handle_y = 0; } } diff --git a/src/render.c b/src/render.c index 20ab142..a242504 100644 --- a/src/render.c +++ b/src/render.c @@ -224,7 +224,7 @@ static void RenderTable(const uiTable* table, RenderState* state) { state->subsurface.width = table->widths[col]; state->pen.x = 0; - const uiCell* cell = GetCell(table, row, col); + const uiCell* cell = TableGetCell(table, row, col); RenderText(string_data(cell->text), string_length(cell->text), state); // Reset the original subsurface and pen for subsequent columns. @@ -242,14 +242,11 @@ static void RenderTable(const uiTable* table, RenderState* state) { if (table->flags.vertical_overflow) { state->pen.x = col_widths_sum; - const int y_start = (int)((double)table->offset / (double)table->rows * - (double)table->height); - - const int height = (int)((double)table->num_visible_rows / - (double)table->rows * (double)table->height); - + const uiScrollbar* scrollbar = &table->scrollbar; FillRect( - &(uiRect){.y = y_start, .width = ScrollBarWidth, .height = height}, + &(uiRect){.y = scrollbar->handle_y, + .width = ScrollbarWidth, + .height = scrollbar->handle_height}, uiPink, state); state->pen.x = x0; diff --git a/src/uiLibrary.h b/src/uiLibrary.h index 98719d7..f6faf01 100644 --- a/src/uiLibrary.h +++ b/src/uiLibrary.h @@ -6,8 +6,31 @@ #include +#ifndef NDEBUG +#include +#define UI_LOG(...) printf("[ui] " __VA_ARGS__ "\n") +#else +#define UI_LOG +#endif + +/// Input handling of scrollbars. +typedef struct uiScrollState { + int scrollbar_handle_y_start; // y-coordinate of the scrollbar handle at the + // start of the scroll. + int table_offset_start; // Row offset of a table at the start of the scroll. + bool scrolling; // Whether a scrollbar is being dragged. +} uiScrollState; + +/// Input handling of mouse-down events. +typedef struct uiMouseDownState { + uiPtr widget; // The widget currently processing a mouse down event. + uiPoint start_point; // Mouse position when the mouse went down. +} uiMouseDownState; + typedef struct uiLibrary { FontAtlas* font; + uiScrollState scroll; + uiMouseDownState mouse_down; uiMouseButtonState mouse_button_state[uiMouseButtonMax]; uiWidgetEvent widget_events[MaxWidgetEvents]; int num_widget_events; diff --git a/src/widget/scrollbar.c b/src/widget/scrollbar.c new file mode 100644 index 0000000..9cece5d --- /dev/null +++ b/src/widget/scrollbar.c @@ -0,0 +1,8 @@ +#include "widget.h" + +int ScrollbarScroll(uiScrollbar* scrollbar, int y) { + assert(scrollbar); + scrollbar->handle_y = + Max(0, Min(scrollbar->height - scrollbar->handle_height, y)); + return scrollbar->handle_y; +} diff --git a/src/widget/table.c b/src/widget/table.c index 76a0413..e7d412e 100644 --- a/src/widget/table.c +++ b/src/widget/table.c @@ -1,20 +1,7 @@ #include "widget.h" -const uiCell* GetCell(const uiTable* table, int row, int col) { - assert(table); - return &table->cells[row][col]; -} - -uiCell* GetCellMut(uiTable* table, int row, int col) { - assert(table); - return (uiCell*)GetCell(table, row, col); -} - -uiCell** GetLastRow(uiTable* table) { - assert(table); - assert(table->rows > 0); - return &table->cells[table->rows - 1]; -} +#define Min(a, b) ((a) < (b) ? (a) : (b)) +#define Max(a, b) ((a) > (b) ? (a) : (b)) uiTable* uiMakeTable(int rows, int cols, const char** header) { uiTable* table = UI_NEW(uiTable); @@ -73,7 +60,7 @@ void uiTableAddRow(uiTable* table, const char** row) { ASSERT(cells); table->cells = cells; - uiCell** pLastRow = GetLastRow(table); + uiCell** pLastRow = TableGetLastRow(table); *pLastRow = calloc(table->cols, sizeof(uiCell)); ASSERT(*pLastRow); uiCell* lastRow = *pLastRow; @@ -86,10 +73,45 @@ void uiTableAddRow(uiTable* table, const char** row) { void uiTableSet(uiTable* table, int row, int col, const char* text) { assert(table); assert(text); - GetCellMut(table, row, col)->text = string_new(text); + TableGetCellMut(table, row, col)->text = string_new(text); } const char* uiTableGet(const uiTable* table, int row, int col) { assert(table); - return string_data(GetCell(table, row, col)->text); + return string_data(TableGetCell(table, row, col)->text); +} + +void uiTableScroll(uiTable* table, int row) { + assert(table); + table->offset = Min(table->rows - table->num_visible_rows, Max(0, row)); + SyncScrollbarToTable(table); +} + +void SyncScrollbarToTable(uiTable* table) { + assert(table); + ScrollbarScroll( + &table->scrollbar, (int)((double)table->offset / (double)table->rows * + (double)table->height)); +} + +void SyncTableToScrollbar(uiTable* table) { + assert(table); + table->offset = (int)((double)table->scrollbar.handle_y / + (double)table->height * (double)table->rows); +} + +const uiCell* TableGetCell(const uiTable* table, int row, int col) { + assert(table); + return &table->cells[row][col]; +} + +uiCell* TableGetCellMut(uiTable* table, int row, int col) { + assert(table); + return (uiCell*)TableGetCell(table, row, col); +} + +uiCell** TableGetLastRow(uiTable* table) { + assert(table); + assert(table->rows > 0); + return &table->cells[table->rows - 1]; } diff --git a/src/widget/widget.c b/src/widget/widget.c index ef79ac4..ebcaf10 100644 --- a/src/widget/widget.c +++ b/src/widget/widget.c @@ -63,11 +63,28 @@ uiPtr uiMakeTablePtr(uiTable* table) { return (uiPtr){.type = uiTypeTable, .table = table}; } -static uiPtr uiMakeWidgetPtr(uiWidget* widget) { +uiPtr uiMakeWidgetPtr(uiWidget* widget) { assert(widget); - return (uiPtr){.type = widget->type, .widget = widget}; + switch (widget->type) { + case uiTypeButton: + return uiMakeButtonPtr((uiButton*)widget); + case uiTypeFrame: + return uiMakeFramePtr((uiFrame*)widget); + case uiTypeLabel: + return uiMakeLabelPtr((uiLabel*)widget); + case uiTypeTable: + return uiMakeTablePtr((uiTable*)widget); + default: + ASSERT(false); + break; + } + return (uiPtr){0}; } +uiPtr uiNullptr(void) { return (uiPtr){0}; } + +bool uiIsNullptr(uiPtr ptr) { return ptr.widget == 0; } + uiButton* uiGetButtonPtr(uiPtr ptr) { assert(ptr.type == uiTypeButton); assert(ptr.button); diff --git a/src/widget/widget.h b/src/widget/widget.h index 63f3d94..db11164 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -16,39 +16,42 @@ typedef struct uiWidget { Widget_list children; } uiWidget; -/// Button. typedef struct uiButton { uiWidget widget; string text; } uiButton; -/// Frame. typedef struct uiFrame { uiWidget widget; } uiFrame; -/// Label. typedef struct uiLabel { uiWidget widget; string text; } uiLabel; -/// Table cell. +typedef struct uiScrollbar { + int width; + int height; // Total height: handle plus scrollable area. + int handle_height; // Height of the scroll handle. + int handle_y; // Starting y-coordinate of the handle. +} uiScrollbar; + typedef struct uiCell { string text; } uiCell; -/// Table. typedef struct uiTable { - uiWidget widget; - int rows; - int cols; - int height; // Height in pixels. - int* widths; // Width, in pixels, for each column. - uiCell* header; // If non-null, row of 'cols' header cells. - uiCell** cells; // Array of 'rows' rows, each of 'cols' cells. - int offset; // Offset into the rows of the table. Units: rows. - int num_visible_rows; // The number of rows that are visible at once. + uiWidget widget; + int rows; + int cols; + int height; // Height in pixels. + int* widths; // Width, in pixels, for each column. + uiCell* header; // If non-null, row of 'cols' header cells. + uiCell** cells; // Array of 'rows' rows, each of 'cols' cells. + int offset; // Offset into the rows of the table. Units: rows. + int num_visible_rows; // The number of rows that are visible at once. + uiScrollbar scrollbar; struct { bool vertical_overflow : 1; // True if contents overflow vertically. } flags; @@ -56,14 +59,24 @@ typedef struct uiTable { void DestroyWidget(uiWidget** ppWidget); -const uiCell* GetCell(const uiTable* table, int row, int col); -uiCell* GetCellMut(uiTable* table, int row, int col); -uiCell** GetLastRow(uiTable* table); +/// Set the scrollbar handle's y-coordinate, which is clipped to the scrollbar's +/// rectangle. +/// +/// Return the handle's y-coordinate after clipping. +int ScrollbarScroll(uiScrollbar*, int y); -#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) +const uiCell* TableGetCell(const uiTable*, int row, int col); +uiCell* TableGetCellMut(uiTable*, int row, int col); +uiCell** TableGetLastRow(uiTable*); +void SyncScrollbarToTable(uiTable*); +void SyncTableToScrollbar(uiTable*); + +static inline int Min(int a, int b) { return a < b ? a : b; } +static inline int Max(int a, int b) { return a > b ? a : b; } static inline void* uiAlloc(size_t count, size_t size) { void* mem = calloc(count, size); ASSERT(mem); return mem; } +#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) -- cgit v1.2.3