diff --git a/fiv-browser.c b/fiv-browser.c index 568401a..1707e46 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -103,6 +103,7 @@ struct item { struct row { Item *items; ///< Ends with a NULL entry + gsize len; ///< Length of items int x_offset; ///< Start position outside borders int y_offset; ///< Start position inside borders }; @@ -122,11 +123,9 @@ append_row(FivBrowser *self, int *y, int x, GArray *items_array) *y += self->item_spacing; *y += self->item_border_y; - g_array_append_val(self->layouted_rows, ((Row) { - .items = g_array_steal(items_array, NULL), - .x_offset = x, - .y_offset = *y, - })); + Row row = {.x_offset = x, .y_offset = *y}; + row.items = g_array_steal(items_array, &row.len), + g_array_append_val(self->layouted_rows, row); // Not trying to pack them vertically, but this would be the place to do it. *y += self->item_height; @@ -1174,13 +1173,172 @@ fiv_browser_scroll_event(GtkWidget *widget, GdkEventScroll *event) } } +static void +select_closest(FivBrowser *self, const Row *row, int target) +{ + int closest = G_MAXINT; + for (guint i = 0; i < row->len; i++) { + GdkRectangle extents = item_extents(self, row->items + i, row); + int distance = ABS(extents.x + extents.width / 2 - target); + if (distance > closest) + break; + self->selected = row->items[i].entry; + closest = distance; + } +} + +static void +scroll_to_row(FivBrowser *self, const Row *row) +{ + if (!self->vadjustment) + return; + + double y1 = gtk_adjustment_get_value(self->vadjustment); + double ph = gtk_adjustment_get_page_size(self->vadjustment); + if (row->y_offset < y1) { + gtk_adjustment_set_value( + self->vadjustment, row->y_offset - self->item_border_y); + } else if (row->y_offset + self->item_height > y1 + ph) { + gtk_adjustment_set_value(self->vadjustment, + row->y_offset - ph + self->item_height + self->item_border_y); + } +} + +static void +move_selection(FivBrowser *self, GtkDirectionType dir) +{ + GtkWidget *widget = GTK_WIDGET(self); + if (!self->layouted_rows->len) + return; + + const Row *selected_row = NULL; + if (!self->selected) { + switch (dir) { + case GTK_DIR_RIGHT: + case GTK_DIR_DOWN: + selected_row = &g_array_index(self->layouted_rows, Row, 0); + self->selected = selected_row->items->entry; + goto adjust; + case GTK_DIR_LEFT: + case GTK_DIR_UP: + selected_row = &g_array_index( + self->layouted_rows, Row, self->layouted_rows->len - 1); + self->selected = selected_row->items[selected_row->len - 1].entry; + goto adjust; + default: + g_assert_not_reached(); + } + } + + gsize x = 0, y = 0; + int target_offset = 0; + for (y = 0; y < self->layouted_rows->len; y++) { + const Row *row = &g_array_index(self->layouted_rows, Row, y); + for (x = 0; x < row->len; x++) { + const Item *item = row->items + x; + if (item->entry == self->selected) { + GdkRectangle extents = item_extents(self, item, row); + target_offset = extents.x + extents.width / 2; + goto found; + } + } + } +found: + g_return_if_fail(y < self->layouted_rows->len); + selected_row = &g_array_index(self->layouted_rows, Row, y); + + switch (dir) { + case GTK_DIR_LEFT: + if (x > 0) { + self->selected = selected_row->items[--x].entry; + } else if (y-- > 0) { + selected_row = &g_array_index(self->layouted_rows, Row, y); + self->selected = selected_row->items[selected_row->len - 1].entry; + } + break; + case GTK_DIR_RIGHT: + if (++x < selected_row->len) { + self->selected = selected_row->items[x].entry; + } else if (++y < self->layouted_rows->len) { + selected_row = &g_array_index(self->layouted_rows, Row, y); + self->selected = selected_row->items[0].entry; + } + break; + case GTK_DIR_UP: + if (y-- > 0) { + selected_row = &g_array_index(self->layouted_rows, Row, y); + select_closest(self, selected_row, target_offset); + } + break; + case GTK_DIR_DOWN: + if (++y < self->layouted_rows->len) { + selected_row = &g_array_index(self->layouted_rows, Row, y); + select_closest(self, selected_row, target_offset); + } + break; + default: + g_assert_not_reached(); + } + +adjust: + // TODO(p): We should also do it horizontally, although we don't use it. + scroll_to_row(self, selected_row); + gtk_widget_queue_draw(widget); +} + +static void +move_selection_home(FivBrowser *self) +{ + if (self->layouted_rows->len) { + const Row *row = &g_array_index(self->layouted_rows, Row, 0); + self->selected = row->items[0].entry; + scroll_to_row(self, row); + gtk_widget_queue_draw(GTK_WIDGET(self)); + } +} + +static void +move_selection_end(FivBrowser *self) +{ + if (self->layouted_rows->len) { + const Row *row = &g_array_index( + self->layouted_rows, Row, self->layouted_rows->len - 1); + self->selected = row->items[row->len - 1].entry; + scroll_to_row(self, row); + gtk_widget_queue_draw(GTK_WIDGET(self)); + } +} + static gboolean fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event) { FivBrowser *self = FIV_BROWSER(widget); - if (!(event->state & gtk_accelerator_get_default_mod_mask()) && - event->keyval == GDK_KEY_Return && self->selected) - return open_entry(widget, self->selected, FALSE); + if (!(event->state & gtk_accelerator_get_default_mod_mask())) { + switch (event->keyval) { + case GDK_KEY_Return: + if (self->selected) + return open_entry(widget, self->selected, FALSE); + return TRUE; + case GDK_KEY_Left: + move_selection(self, GTK_DIR_LEFT); + return TRUE; + case GDK_KEY_Right: + move_selection(self, GTK_DIR_RIGHT); + return TRUE; + case GDK_KEY_Up: + move_selection(self, GTK_DIR_UP); + return TRUE; + case GDK_KEY_Down: + move_selection(self, GTK_DIR_DOWN); + return TRUE; + case GDK_KEY_Home: + move_selection_home(self); + return TRUE; + case GDK_KEY_End: + move_selection_end(self); + return TRUE; + } + } return GTK_WIDGET_CLASS(fiv_browser_parent_class) ->key_press_event(widget, event);