diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a69726..929c83e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ * **csd.preferred** can now be set to `none` to disable window decorations. Note that some compositors will render SSDs despite this option being used (https://codeberg.org/dnkl/foot/issues/163). +* Terminal content is now auto-scrolled when moving the mouse above or + below the window while selecting + (https://codeberg.org/dnkl/foot/issues/149). + ### Changed ### Deprecated diff --git a/input.c b/input.c index 20c1381c..f3993978 100644 --- a/input.c +++ b/input.c @@ -1325,10 +1325,46 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, = old_col != seat->mouse.col || old_row != seat->mouse.row; /* Cursor is inside the grid, i.e. *not* in the margins */ - bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + enum selection_scroll_direction auto_scroll_direction + = y < term->margins.top ? SELECTION_SCROLL_UP + : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN + : SELECTION_SCROLL_NOT; + + if (auto_scroll_direction == SELECTION_SCROLL_NOT) + selection_stop_scroll_timer(term); /* Update selection */ if (!term->is_searching) { + if (auto_scroll_direction != SELECTION_SCROLL_NOT) { + /* + * Start ‘selection auto-scrolling’ + * + * The speed of the scrolling is proportional to the + * distance between the mouse and the grid; the + * further away the mouse is, the faster we scroll. + * + * Note that the speed is measured in ‘intervals (in + * ns) between each timed scroll of a single line’. + * + * Thus, the further away the mouse is, the smaller + * interval value we use. + */ + + int distance = auto_scroll_direction == SELECTION_SCROLL_UP + ? term->margins.top - y + : y - (term->height - term->margins.bottom); + + assert(distance > 0); + int divisor + = distance * term->conf->scrollback.multiplier / term->scale; + + selection_start_scroll_timer( + term, 400000000 / (divisor > 0 ? divisor : 1), + auto_scroll_direction, selection_col); + } + if (cursor_is_on_new_cell || term->selection.end.row < 0) selection_update(term, selection_col, selection_row); } diff --git a/selection.c b/selection.c index 7475f229..2d40b38b 100644 --- a/selection.c +++ b/selection.c @@ -15,6 +15,7 @@ #include "log.h" #include "async.h" +#include "commands.h" #include "config.h" #include "extract.h" #include "grid.h" @@ -619,6 +620,7 @@ selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) if (!term->selection.ongoing) return; + selection_stop_scroll_timer(term); term->selection.ongoing = false; if (term->selection.start.row < 0 || term->selection.end.row < 0) @@ -647,6 +649,7 @@ selection_cancel(struct terminal *term) term->selection.start.row, term->selection.start.col, term->selection.end.row, term->selection.end.col); + selection_stop_scroll_timer(term); if (term->selection.start.row >= 0 && term->selection.end.row >= 0) { foreach_selected( @@ -789,6 +792,108 @@ selection_mark_row( selection_finalize(seat, term, serial); } +static bool +fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t expiration_count; + ssize_t ret = read( + term->selection.auto_scroll.fd, + &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read selection scroll timer"); + return false; + } + + switch (term->selection.auto_scroll.direction) { + case SELECTION_SCROLL_NOT: + return true; + + case SELECTION_SCROLL_UP: + cmd_scrollback_up(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, 0); + break; + + case SELECTION_SCROLL_DOWN: + cmd_scrollback_down(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, term->rows - 1); + break; + } + + + return true; +} + +void +selection_start_scroll_timer(struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col) +{ + assert(direction != SELECTION_SCROLL_NOT); + + if (!term->selection.ongoing) + return; + + if (term->selection.auto_scroll.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create selection scroll timer"); + goto err; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_scroll_timer, term)) { + close(fd); + return; + } + + term->selection.auto_scroll.fd = fd; + } + + struct itimerspec timer; + if (timerfd_gettime(term->selection.auto_scroll.fd, &timer) < 0) { + LOG_ERRNO("failed to get current selection scroll timer value"); + goto err; + } + + if (timer.it_value.tv_sec == 0 && timer.it_value.tv_nsec == 0) + timer.it_value.tv_nsec = 1; + + timer.it_interval.tv_sec = interval_ns / 1000000000; + timer.it_interval.tv_nsec = interval_ns % 1000000000; + + if (timerfd_settime(term->selection.auto_scroll.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to set new selection scroll timer value"); + goto err; + } + + term->selection.auto_scroll.direction = direction; + term->selection.auto_scroll.col = col; + return; + +err: + selection_stop_scroll_timer(term); + return; +} + +void +selection_stop_scroll_timer(struct terminal *term) +{ + if (term->selection.auto_scroll.fd < 0) { + assert(term->selection.auto_scroll.direction == SELECTION_SCROLL_NOT); + return; + } + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + term->selection.auto_scroll.fd = -1; + term->selection.auto_scroll.direction = SELECTION_SCROLL_NOT; +} static void target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) diff --git a/selection.h b/selection.h index 547da2fc..888b2f05 100644 --- a/selection.h +++ b/selection.h @@ -74,3 +74,8 @@ void text_from_primary( struct seat *seat, struct terminal *term, void (*cb)(const char *data, size_t size, void *user), void (*dont)(void *user), void *user); + +void selection_start_scroll_timer( + struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col); +void selection_stop_scroll_timer(struct terminal *term); diff --git a/terminal.c b/terminal.c index 7aacb58c..c1f8c06c 100644 --- a/terminal.c +++ b/terminal.c @@ -985,6 +985,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .selection = { .start = {-1, -1}, .end = {-1, -1}, + .auto_scroll = { + .fd = -1, + }, }, .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, @@ -1164,6 +1167,7 @@ term_shutdown(struct terminal *term) term_cursor_blink_disable(term); + fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1176,6 +1180,7 @@ term_shutdown(struct terminal *term) else close(term->ptmx); + term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_fd = -1; @@ -1226,6 +1231,7 @@ term_destroy(struct terminal *term) } } + fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); diff --git a/terminal.h b/terminal.h index 655cb9ce..fd4ac840 100644 --- a/terminal.h +++ b/terminal.h @@ -182,6 +182,7 @@ enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR }; enum selection_kind { SELECTION_NONE, SELECTION_NORMAL, SELECTION_BLOCK }; enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; +enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; struct ptmx_buffer { void *data; @@ -349,6 +350,12 @@ struct terminal { struct coord start; struct coord end; bool ongoing; + + struct { + int fd; + int col; + enum selection_scroll_direction direction; + } auto_scroll; } selection; bool is_searching;