/*
 * rofi
 *
 * MIT/X11 License
 * Copyright © 2013-2021 Qball Cow <qball@gmpclient.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

#include <config.h>
#include <glib.h>
#include <widgets/box.h>
#include <widgets/icon.h>
#include <widgets/listview.h>
#include <widgets/scrollbar.h>
#include <widgets/textbox.h>
#include <widgets/widget.h>

#include "settings.h"
#include "theme.h"

#include "timings.h"

/** Default spacing between the elements in the listview. */
#define DEFAULT_SPACING 2

/**
 * Orientation of the listview
 */
/** Vertical (classical) list */
#define LISTVIEW ROFI_ORIENTATION_VERTICAL
/** Horizontal list. (barview) */
#define BARVIEW ROFI_ORIENTATION_HORIZONTAL

/**
 * The moving direction of the selection, this (in barview) affects the
 * scrolling.
 */
typedef enum { LEFT_TO_RIGHT = 0, RIGHT_TO_LEFT = 1 } MoveDirection;

typedef struct {
  box *box;
  textbox *textbox;
  textbox *index;
  icon *icon;
} _listview_row;

struct _listview {
  widget widget;

  RofiOrientation type;

  // RChanged
  // Text needs to be repainted.
  unsigned int rchanged;
  // Administration

  unsigned int cur_page;
  unsigned int last_offset;
  unsigned int selected;

  unsigned int element_height;
  unsigned int max_rows;
  unsigned int max_elements;

  //
  gboolean fixed_columns;
  unsigned int cur_columns;
  unsigned int req_elements;
  unsigned int cur_elements;

  RofiDistance spacing;
  unsigned int menu_lines;
  unsigned int max_displayed_lines;
  unsigned int menu_columns;
  unsigned int fixed_num_lines;
  unsigned int dynamic;
  unsigned int eh;
  unsigned int reverse;
  gboolean cycle;
  gboolean multi_select;

  ScrollType scroll_type;

  _listview_row *boxes;
  scrollbar *scrollbar;

  listview_update_callback callback;
  void *udata;

  gboolean scrollbar_scroll;

  xcb_timestamp_t last_click;
  listview_mouse_activated_cb mouse_activated;
  void *mouse_activated_data;

  char *listview_name;

  PangoEllipsizeMode emode;
  /** Barview */
  struct {
    MoveDirection direction;
    unsigned int cur_visible;
  } barview;
};
/**
 * Names used for theming the elements in the listview.
 * Each row can have 3 modes,  normal, selected and alternate.
 * Each row can have 3 states, normal, urgent and active.
 */
const char *const listview_theme_prop_names[][3] = {
    /** Normal row */
    {"normal.normal", "selected.normal", "alternate.normal"},
    /** Urgent row */
    {"normal.urgent", "selected.urgent", "alternate.urgent"},
    /** Active row */
    {"normal.active", "selected.active", "alternate.active"},
};

static void listview_set_state(_listview_row r, TextBoxFontType tbft) {
  widget *w = WIDGET(r.box);
  TextBoxFontType t = tbft & STATE_MASK;
  if (w == NULL) {
    return;
  }
  // ACTIVE has priority over URGENT if both set.
  if (t == (URGENT | ACTIVE)) {
    t = ACTIVE;
  }
  switch ((tbft & FMOD_MASK)) {
  case HIGHLIGHT:
    widget_set_state(w, listview_theme_prop_names[t][1]);
    break;
  case ALT:
    widget_set_state(w, listview_theme_prop_names[t][2]);
    break;
  default:
    widget_set_state(w, listview_theme_prop_names[t][0]);
    break;
  }
}
static void listview_add_widget(listview *lv, _listview_row *row, widget *wid,
                                const char *label) {
  TextboxFlags flags = (lv->multi_select) ? TB_INDICATOR : 0;
  if (strcasecmp(label, "element-icon") == 0) {
    row->icon = icon_create(WIDGET(wid), "element-icon");
    box_add((box *)wid, WIDGET(row->icon), FALSE);
  } else if (strcasecmp(label, "element-text") == 0) {
    row->textbox =
        textbox_create(WIDGET(wid), WIDGET_TYPE_TEXTBOX_TEXT, "element-text",
                       TB_AUTOHEIGHT | flags, NORMAL, "DDD", 0, 0);
    box_add((box *)wid, WIDGET(row->textbox), TRUE);
  } else if (strcasecmp(label, "element-index") == 0) {
    row->index =
        textbox_create(WIDGET(wid), WIDGET_TYPE_TEXTBOX_TEXT, "element-index",
                       TB_AUTOHEIGHT, NORMAL, " ", 0, 0);
    box_add((box *)wid, WIDGET(row->index), FALSE);
  } else {
    widget *wid2 = (widget *)box_create(wid, label, ROFI_ORIENTATION_VERTICAL);
    box_add((box *)wid, WIDGET(wid2), TRUE);
    GList *list = rofi_theme_get_list_strings(
        WIDGET(wid2),
        "children"); // rofi_theme_get_list(WIDGET(wid2), "children", "");
    for (GList *iter = g_list_first(list); iter != NULL;
         iter = g_list_next(iter)) {
      listview_add_widget(lv, row, wid2, (const char *)iter->data);
    }
  }
}

static void listview_create_row(listview *lv, _listview_row *row) {
  row->box = box_create(WIDGET(lv), "element", ROFI_ORIENTATION_HORIZONTAL);
  widget_set_type(WIDGET(row->box), WIDGET_TYPE_LISTVIEW_ELEMENT);
  GList *list = NULL;
  list = rofi_theme_get_list_strings(WIDGET(row->box), "children");
  if (list == NULL) {
    if (config.show_icons) {
      list = g_list_append(list, g_strdup("element-icon"));
      list = g_list_append(list, g_strdup("element-text"));
    } else {
      list = g_list_append(list, g_strdup("element-text"));
    }
  }

  row->textbox = NULL;
  row->icon = NULL;
  row->index = NULL;

  for (GList *iter = g_list_first(list); iter != NULL;
       iter = g_list_next(iter)) {
    listview_add_widget(lv, row, WIDGET(row->box), (const char *)iter->data);
  }
  g_list_free_full(list, g_free);
}

static int listview_get_desired_height(widget *wid, const int width);

static void listview_free(widget *wid) {
  listview *lv = (listview *)wid;
  for (unsigned int i = 0; i < lv->cur_elements; i++) {
    widget_free(WIDGET(lv->boxes[i].box));
  }
  g_free(lv->boxes);

  g_free(lv->listview_name);
  widget_free(WIDGET(lv->scrollbar));
  g_free(lv);
}
static unsigned int scroll_per_page_barview(listview *lv) {
  unsigned int offset = lv->last_offset;

  // selected row is always visible.
  // If selected is visible do not scroll.
  if (lv->selected < lv->last_offset) {
    offset = lv->selected;
    lv->rchanged = TRUE;
  } else if (lv->selected >= (lv->last_offset + lv->barview.cur_visible)) {
    offset = lv->selected;
    lv->rchanged = TRUE;
  }
  return offset;
}
static unsigned int scroll_per_page(listview *lv) {
  int offset = 0;

  // selected row is always visible.
  // If selected is visible do not scroll.
  if (((lv->selected - (lv->last_offset)) < (lv->max_elements)) &&
      (lv->selected >= (lv->last_offset))) {
    offset = lv->last_offset;
  } else {
    // Do paginating
    unsigned int page =
        (lv->max_elements > 0) ? (lv->selected / lv->max_elements) : 0;
    offset = page * lv->max_elements;
    if (page != lv->cur_page) {
      lv->cur_page = page;
      lv->rchanged = TRUE;
    }
    // Set the position
    // scrollbar_set_handle ( lv->scrollbar, page * lv->max_elements );
  }
  return offset;
}

static unsigned int scroll_continious(listview *lv) {
  unsigned int middle = (lv->max_rows - ((lv->max_rows & 1) == 0)) / 2;
  unsigned int offset = 0;
  if (lv->selected > middle) {
    if (lv->selected < (lv->req_elements - (lv->max_rows - middle))) {
      offset = lv->selected - middle;
    }
    // Don't go below zero.
    else if (lv->req_elements > lv->max_rows) {
      offset = lv->req_elements - lv->max_rows;
    }
  }
  if (offset != lv->cur_page) {
    // scrollbar_set_handle ( lv->scrollbar, offset );
    lv->cur_page = offset;
    lv->rchanged = TRUE;
  }
  return offset;
}

static void update_element(listview *lv, unsigned int tb, unsigned int index,
                           gboolean full) {
  // Select drawing mode
  TextBoxFontType type = (index & 1) == 0 ? NORMAL : ALT;
  type = (index) == lv->selected ? HIGHLIGHT : type;

  if (lv->boxes[tb].index) {
    if (index < 10) {
      char str[2] = {((index + 1) % 10) + '0', '\0'};
      textbox_text(lv->boxes[tb].index, str);
    } else {
      textbox_text(lv->boxes[tb].index, " ");
    }
  }
  if (lv->callback) {
    lv->callback(lv->boxes[tb].textbox, lv->boxes[tb].icon, index, lv->udata,
                 &type, full);
    listview_set_state(lv->boxes[tb], type);
  }
}

static void barview_draw(widget *wid, cairo_t *draw) {
  unsigned int offset = 0;
  listview *lv = (listview *)wid;
  offset = scroll_per_page_barview(lv);
  lv->last_offset = offset;
  int spacing_hori =
      distance_get_pixel(lv->spacing, ROFI_ORIENTATION_HORIZONTAL);

  int left_offset = widget_padding_get_left(wid);
  int right_offset = lv->widget.w - widget_padding_get_right(wid);
  int top_offset = widget_padding_get_top(wid);
  if (lv->cur_elements > 0) {
    // Set new x/y position.
    unsigned int max = MIN(lv->cur_elements, lv->req_elements - offset);
    if (lv->rchanged) {
      int first = TRUE;
      int width = lv->widget.w;
      lv->barview.cur_visible = 0;
      width -= widget_padding_get_padding_width(wid);
      if (lv->barview.direction == LEFT_TO_RIGHT) {
        for (unsigned int i = 0; i < max && width > 0; i++) {
          update_element(lv, i, i + offset, TRUE);
          int twidth = widget_get_desired_width(WIDGET(lv->boxes[i].box),
                                                lv->element_height);
          if (twidth >= width) {
            if (!first) {
              break;
            }
            twidth = width;
          }
          widget_move(WIDGET(lv->boxes[i].box), left_offset, top_offset);
          widget_resize(WIDGET(lv->boxes[i].box), twidth, lv->element_height);

          widget_draw(WIDGET(lv->boxes[i].box), draw);
          width -= twidth + spacing_hori;
          left_offset += twidth + spacing_hori;
          first = FALSE;
          lv->barview.cur_visible++;
        }
      } else {
        for (unsigned int i = 0;
             i < lv->cur_elements && width > 0 && i <= offset; i++) {
          update_element(lv, i, offset - i, TRUE);
          int twidth = widget_get_desired_width(WIDGET(lv->boxes[i].box),
                                                lv->element_height);
          if (twidth >= width) {
            if (!first) {
              break;
            }
            twidth = width;
          }
          right_offset -= twidth;
          widget_move(WIDGET(lv->boxes[i].box), right_offset, top_offset);
          widget_resize(WIDGET(lv->boxes[i].box), twidth, lv->element_height);

          widget_draw(WIDGET(lv->boxes[i].box), draw);
          width -= twidth + spacing_hori;
          right_offset -= spacing_hori;
          first = FALSE;
          lv->barview.cur_visible++;
        }
        offset -= lv->barview.cur_visible - 1;
        lv->last_offset = offset;
        for (unsigned int i = 0; i < (lv->barview.cur_visible / 2); i++) {
          _listview_row temp = lv->boxes[i];
          int sw = lv->barview.cur_visible - i - 1;
          lv->boxes[i] = lv->boxes[sw];
          lv->boxes[sw] = temp;
        }
      }
      lv->rchanged = FALSE;
    } else {
      for (unsigned int i = 0; i < lv->barview.cur_visible; i++) {
        update_element(lv, i, i + offset, TRUE);
        widget_draw(WIDGET(lv->boxes[i].box), draw);
      }
    }
  }
}

static void listview_draw(widget *wid, cairo_t *draw) {
  unsigned int offset = 0;
  listview *lv = (listview *)wid;
  if (lv->scroll_type == LISTVIEW_SCROLL_CONTINIOUS) {
    offset = scroll_continious(lv);
  } else {
    offset = scroll_per_page(lv);
  }
  // Set these all together to make sure they update consistently.
  scrollbar_set_max_value(lv->scrollbar, lv->req_elements);
  scrollbar_set_handle_length(lv->scrollbar, lv->cur_columns * lv->max_rows);
  if (lv->reverse) {
    scrollbar_set_handle(lv->scrollbar, lv->req_elements - lv->selected - 1);
  } else {
    scrollbar_set_handle(lv->scrollbar, lv->selected);
  }
  lv->last_offset = offset;
  int spacing_vert = distance_get_pixel(lv->spacing, ROFI_ORIENTATION_VERTICAL);
  int spacing_hori =
      distance_get_pixel(lv->spacing, ROFI_ORIENTATION_HORIZONTAL);

  int left_offset = widget_padding_get_left(wid);
  int top_offset = widget_padding_get_top(wid);
  /*
     if ( lv->scrollbar->widget.index == 0 ) {
      left_offset += spacing_hori + lv->scrollbar->widget.w;
     }
   */
  if (lv->cur_elements > 0 && lv->max_rows > 0) {
    // Set new x/y position.
    unsigned int max = MIN(lv->cur_elements, lv->req_elements - offset);
    if (lv->rchanged) {
      unsigned int width = lv->widget.w;
      width -= widget_padding_get_padding_width(wid);
      if (widget_enabled(WIDGET(lv->scrollbar))) {
        width -= spacing_hori;
        width -= widget_get_width(WIDGET(lv->scrollbar));
      }
      unsigned int element_width =
          (width - spacing_hori * (lv->cur_columns - 1)) / lv->cur_columns;

      int d = width - (element_width + spacing_hori) * (lv->cur_columns - 1) -
              element_width;
      if (lv->cur_columns > 1) {
        int diff = d / (lv->cur_columns - 1);
        if (diff >= 1) {
          spacing_hori += 1;
          d -= lv->cur_columns - 1;
        }
      }
      for (unsigned int i = 0; i < max; i++) {
        unsigned int ex =
            left_offset + ((i) / lv->max_rows) * (element_width + spacing_hori);

        if ((i) / lv->max_rows == (lv->cur_columns - 1)) {
          ex += d;
        }
        if (lv->reverse) {
          unsigned int ey =
              wid->h -
              (widget_padding_get_bottom(wid) +
               ((i) % lv->max_rows) * (lv->element_height + spacing_vert)) -
              lv->element_height;
          widget_move(WIDGET(lv->boxes[i].box), ex, ey);
          widget_resize(WIDGET(lv->boxes[i].box), element_width,
                        lv->element_height);
        } else {
          unsigned int ey =
              top_offset +
              ((i) % lv->max_rows) * (lv->element_height + spacing_vert);
          widget_move(WIDGET(lv->boxes[i].box), ex, ey);
          widget_resize(WIDGET(lv->boxes[i].box), element_width,
                        lv->element_height);
        }

        update_element(lv, i, i + offset, TRUE);
        widget_draw(WIDGET(lv->boxes[i].box), draw);
      }
      lv->rchanged = FALSE;
    } else {
      for (unsigned int i = 0; i < max; i++) {
        update_element(lv, i, i + offset, TRUE);
        widget_draw(WIDGET(lv->boxes[i].box), draw);
      }
    }
  }
  widget_draw(WIDGET(lv->scrollbar), draw);
}
static WidgetTriggerActionResult
listview_element_trigger_action(widget *wid,
                                MouseBindingListviewElementAction action,
                                gint x, gint y, void *user_data);
static gboolean listview_element_motion_notify(widget *wid, gint x, gint y);

static void _listview_draw(widget *wid, cairo_t *draw) {
  listview *lv = (listview *)wid;
  if (lv->type == LISTVIEW) {
    listview_draw(wid, draw);
  } else {
    barview_draw(wid, draw);
  }
}
/**
 * State names used for theming.
 */
static void listview_recompute_elements(listview *lv) {
  unsigned int newne = 0;
  if (lv->max_rows == 0) {
    return;
  }
  if (!(lv->fixed_columns) && lv->req_elements < lv->max_elements) {
    newne = lv->req_elements;
    lv->cur_columns = (lv->req_elements + (lv->max_rows - 1)) / lv->max_rows;
  } else {
    newne = MIN(lv->req_elements, lv->max_elements);
    lv->cur_columns = lv->menu_columns;
  }
  for (unsigned int i = newne; i < lv->cur_elements; i++) {
    widget_free(WIDGET(lv->boxes[i].box));
  }
  lv->boxes = g_realloc(lv->boxes, newne * sizeof(_listview_row));
  if (newne > 0) {
    for (unsigned int i = lv->cur_elements; i < newne; i++) {
      listview_create_row(lv, &(lv->boxes[i]));
      widget *wid = WIDGET(lv->boxes[i].box);
      widget_set_trigger_action_handler(wid, listview_element_trigger_action,
                                        lv);
      if (wid != NULL) {
        wid->motion_notify = listview_element_motion_notify;
      }

      listview_set_state(lv->boxes[i], NORMAL);
    }
  }
  lv->rchanged = TRUE;
  lv->cur_elements = newne;
}

void listview_set_num_elements(listview *lv, unsigned int rows) {
  if (lv == NULL) {
    return;
  }
  TICK_N("listview_set_num_elements");
  lv->req_elements = rows;
  listview_set_selected(lv, lv->selected);
  TICK_N("Set selected");
  listview_recompute_elements(lv);
  TICK_N("recompute elements");
  widget_queue_redraw(WIDGET(lv));
  TICK_N("queue redraw");
}

unsigned int listview_get_selected(listview *lv) {
  if (lv != NULL) {
    return lv->selected;
  }
  return 0;
}

void listview_set_selected(listview *lv, unsigned int selected) {
  if (lv && lv->req_elements > 0) {
    lv->selected = MIN(selected, lv->req_elements - 1);
    lv->barview.direction = LEFT_TO_RIGHT;
    widget_queue_redraw(WIDGET(lv));
  }
}

static void listview_resize(widget *wid, short w, short h) {
  listview *lv = (listview *)wid;
  lv->widget.w = MAX(0, w);
  lv->widget.h = MAX(0, h);
  int height = lv->widget.h - widget_padding_get_padding_height(WIDGET(lv));
  int spacing_vert = distance_get_pixel(lv->spacing, ROFI_ORIENTATION_VERTICAL);
  lv->max_rows = (spacing_vert + height) / (lv->element_height + spacing_vert);
  lv->max_elements = lv->max_rows * lv->menu_columns;

  widget_move(WIDGET(lv->scrollbar),
              lv->widget.w - widget_padding_get_right(WIDGET(lv)) -
                  widget_get_width(WIDGET(lv->scrollbar)),
              widget_padding_get_top(WIDGET(lv)));

  widget_resize(WIDGET(lv->scrollbar), widget_get_width(WIDGET(lv->scrollbar)),
                height);

  if (lv->type == BARVIEW) {
    lv->max_elements = lv->menu_lines;
  }

  listview_recompute_elements(lv);
  widget_queue_redraw(wid);
}

static widget *listview_find_mouse_target(widget *wid, WidgetType type, gint x,
                                          gint y) {
  widget *target = NULL;
  gint rx, ry;
  listview *lv = (listview *)wid;
  if (widget_enabled(WIDGET(lv->scrollbar)) &&
      widget_intersect(WIDGET(lv->scrollbar), x, y)) {
    rx = x - widget_get_x_pos(WIDGET(lv->scrollbar));
    ry = y - widget_get_y_pos(WIDGET(lv->scrollbar));
    target = widget_find_mouse_target(WIDGET(lv->scrollbar), type, rx, ry);
  }

  unsigned int max = MIN(lv->cur_elements, lv->req_elements - lv->last_offset);
  unsigned int i;
  for (i = 0; i < max && target == NULL; i++) {
    widget *w = WIDGET(lv->boxes[i].box);
    if (widget_intersect(w, x, y)) {
      rx = x - widget_get_x_pos(w);
      ry = y - widget_get_y_pos(w);
      target = widget_find_mouse_target(w, type, rx, ry);
    }
  }

  return target;
}

static WidgetTriggerActionResult
listview_trigger_action(widget *wid, MouseBindingListviewAction action,
                        G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y,
                        G_GNUC_UNUSED void *user_data) {
  listview *lv = (listview *)wid;
  switch (action) {
  case SCROLL_LEFT:
    listview_nav_left(lv);
    break;
  case SCROLL_RIGHT:
    listview_nav_right(lv);
    break;
  case SCROLL_DOWN:
    listview_nav_down(lv);
    break;
  case SCROLL_UP:
    listview_nav_up(lv);
    break;
  }
  return WIDGET_TRIGGER_ACTION_RESULT_HANDLED;
}

static WidgetTriggerActionResult listview_element_trigger_action(
    widget *wid, MouseBindingListviewElementAction action, G_GNUC_UNUSED gint x,
    G_GNUC_UNUSED gint y, void *user_data) {
  listview *lv = (listview *)user_data;
  unsigned int max = MIN(lv->cur_elements, lv->req_elements - lv->last_offset);
  unsigned int i;
  for (i = 0; i < max && WIDGET(lv->boxes[i].box) != wid; i++) {
  }
  if (i == max) {
    return WIDGET_TRIGGER_ACTION_RESULT_IGNORED;
  }

  gboolean custom = FALSE;
  switch (action) {
  case SELECT_HOVERED_ENTRY:
    listview_set_selected(lv, lv->last_offset + i);
    break;
  case ACCEPT_HOVERED_CUSTOM:
    custom = TRUE;
  /* FALLTHRU */
  case ACCEPT_HOVERED_ENTRY:
    listview_set_selected(lv, lv->last_offset + i);
    lv->mouse_activated(lv, custom, lv->mouse_activated_data);
    break;
  }
  return WIDGET_TRIGGER_ACTION_RESULT_HANDLED;
}

static gboolean listview_element_motion_notify(widget *wid,
                                               G_GNUC_UNUSED gint x,
                                               G_GNUC_UNUSED gint y) {
  listview *lv = (listview *)wid->parent;
  unsigned int max = MIN(lv->cur_elements, lv->req_elements - lv->last_offset);
  unsigned int i;
  for (i = 0; i < max && WIDGET(lv->boxes[i].box) != wid; i++) {
  }
  if (i < max && (lv->last_offset + i) != listview_get_selected(lv)) {
    listview_set_selected(lv, lv->last_offset + i);
  }
  return TRUE;
}

listview *listview_create(widget *parent, const char *name,
                          listview_update_callback cb, void *udata,
                          unsigned int eh, gboolean reverse) {
  listview *lv = g_malloc0(sizeof(listview));
  widget_init(WIDGET(lv), parent, WIDGET_TYPE_LISTVIEW, name);
  lv->listview_name = g_strdup(name);
  lv->widget.free = listview_free;
  lv->widget.resize = listview_resize;
  lv->widget.draw = _listview_draw;
  lv->widget.find_mouse_target = listview_find_mouse_target;
  lv->widget.trigger_action = listview_trigger_action;
  lv->widget.get_desired_height = listview_get_desired_height;
  lv->eh = eh;

  lv->emode = PANGO_ELLIPSIZE_END;
  lv->scrollbar = scrollbar_create(WIDGET(lv), "scrollbar");
  // Calculate height of an element.
  //
  _listview_row row;
  listview_create_row(lv, &row);
  // FIXME: hack to scale hight correctly.
  if (lv->eh > 1 && row.textbox) {
    char buff[lv->eh * 2 + 1];
    memset(buff, '\0', lv->eh * 2 + 1);
    for (unsigned int i = 0; i < (lv->eh - 1); i++) {
      buff[i * 2] = 'a';
      buff[i * 2 + 1] = '\n';
    };
    textbox_text(row.textbox, buff);
  }
  // Make textbox very wide.
  lv->element_height = widget_get_desired_height(WIDGET(row.box), 100000);
  widget_free(WIDGET(row.box));

  lv->callback = cb;
  lv->udata = udata;

  // Some settings.
  lv->spacing = rofi_theme_get_distance(WIDGET(lv), "spacing", DEFAULT_SPACING);
  lv->menu_columns =
      rofi_theme_get_integer(WIDGET(lv), "columns", DEFAULT_MENU_COLUMNS);
  lv->fixed_num_lines = rofi_theme_get_boolean(WIDGET(lv), "fixed-height",
                                               config.fixed_num_lines);
  lv->dynamic = rofi_theme_get_boolean(WIDGET(lv), "dynamic", TRUE);
  lv->reverse = rofi_theme_get_boolean(WIDGET(lv), "reverse", reverse);
  lv->cycle = rofi_theme_get_boolean(WIDGET(lv), "cycle", config.cycle);
  lv->fixed_columns =
      rofi_theme_get_boolean(WIDGET(lv), "fixed-columns", FALSE);

  lv->type = rofi_theme_get_orientation(WIDGET(lv), "layout",
                                        ROFI_ORIENTATION_VERTICAL);
  if (lv->type == LISTVIEW) {
    listview_set_show_scrollbar(
        lv, rofi_theme_get_boolean(WIDGET(lv), "scrollbar", FALSE));
  } else {
    listview_set_show_scrollbar(lv, FALSE);
  }
  return lv;
}

/**
 * Navigation commands.
 */

static void listview_nav_up_int(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->req_elements == 0 || (lv->selected == 0 && !lv->cycle)) {
    return;
  }
  if (lv->selected == 0) {
    lv->selected = lv->req_elements;
  }
  lv->selected--;
  lv->barview.direction = RIGHT_TO_LEFT;
  widget_queue_redraw(WIDGET(lv));
}
static void listview_nav_down_int(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->req_elements == 0 ||
      (lv->selected == (lv->req_elements - 1) && !lv->cycle)) {
    return;
  }
  lv->selected = lv->selected < lv->req_elements - 1
                     ? MIN(lv->req_elements - 1, lv->selected + 1)
                     : 0;
  lv->barview.direction = LEFT_TO_RIGHT;
  widget_queue_redraw(WIDGET(lv));
}

void listview_nav_up(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->reverse) {
    listview_nav_down_int(lv);
  } else {
    listview_nav_up_int(lv);
  }
}
void listview_nav_down(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->reverse) {
    listview_nav_up_int(lv);
  } else {
    listview_nav_down_int(lv);
  }
}

void listview_nav_left(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->type == BARVIEW) {
    listview_nav_up_int(lv);
    return;
  }
  if (lv->selected >= lv->max_rows) {
    lv->selected -= lv->max_rows;
    widget_queue_redraw(WIDGET(lv));
  }
}
void listview_nav_right(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->max_rows == 0) {
    return;
  }
  if (lv->type == BARVIEW) {
    listview_nav_down_int(lv);
    return;
  }
  if ((lv->selected + lv->max_rows) < lv->req_elements) {
    lv->selected += lv->max_rows;
    widget_queue_redraw(WIDGET(lv));
  } else if (lv->selected < (lv->req_elements - 1)) {
    // We do not want to move to last item, UNLESS the last column is only
    // partially filled, then we still want to move column and select last
    // entry. First check the column we are currently in.
    int col = lv->selected / lv->max_rows;
    // Check total number of columns.
    int ncol = lv->req_elements / lv->max_rows;
    // If there is an extra column, move.
    if (col != ncol) {
      lv->selected = lv->req_elements - 1;
      widget_queue_redraw(WIDGET(lv));
    }
  }
}

static void listview_nav_page_prev_int(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->type == BARVIEW) {
    if (lv->last_offset == 0) {
      lv->selected = 0;
    } else {
      lv->selected = lv->last_offset - 1;
    }
    lv->barview.direction = RIGHT_TO_LEFT;
    widget_queue_redraw(WIDGET(lv));
    return;
  }

  if (lv->selected < lv->max_elements) {
    lv->selected = 0;
  } else {
    lv->selected -= (lv->max_elements);
  }
  widget_queue_redraw(WIDGET(lv));
}
static void listview_nav_page_next_int(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->req_elements == 0) {
    return;
  }
  if (lv->type == BARVIEW) {
    unsigned int new = lv->last_offset + lv->barview.cur_visible;
    lv->selected = MIN(new, lv->req_elements - 1);
    lv->barview.direction = LEFT_TO_RIGHT;

    widget_queue_redraw(WIDGET(lv));
    return;
  }
  lv->selected += (lv->max_elements);
  if (lv->selected >= lv->req_elements) {
    lv->selected = lv->req_elements - 1;
  }
  widget_queue_redraw(WIDGET(lv));
}

void listview_nav_page_prev(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->reverse) {
    listview_nav_page_next_int(lv);
  } else {
    listview_nav_page_prev_int(lv);
  }
}
void listview_nav_page_next(listview *lv) {
  if (lv == NULL) {
    return;
  }
  if (lv->reverse) {
    listview_nav_page_prev_int(lv);
  } else {
    listview_nav_page_next_int(lv);
  }
}

static int listview_get_desired_height(widget *wid,
                                       G_GNUC_UNUSED const int width) {
  listview *lv = (listview *)wid;
  if (lv == NULL || lv->widget.enabled == FALSE) {
    return 0;
  }
  int spacing = distance_get_pixel(lv->spacing, ROFI_ORIENTATION_VERTICAL);
  int h = lv->menu_lines;
  if (!(lv->fixed_num_lines)) {
    if (lv->dynamic) {
      h = MIN(lv->menu_lines, lv->req_elements);
    } else {
      h = MIN(lv->menu_lines, lv->max_displayed_lines);
    }
  }
  if (lv->type == BARVIEW) {
    h = MIN(h, 1);
  }
  if (h == 0) {
    if (lv->dynamic && !lv->fixed_num_lines) {
      // Hide widget fully.
      return 0;
    }
    return widget_padding_get_padding_height(WIDGET(lv));
  }
  int height = widget_padding_get_padding_height(WIDGET(lv));
  height += h * (lv->element_height + spacing) - spacing;
  return height;
}

void listview_set_show_scrollbar(listview *lv, gboolean enabled) {
  if (lv) {
    if (enabled) {
      widget_enable(WIDGET(lv->scrollbar));
    } else {
      widget_disable(WIDGET(lv->scrollbar));
    }
    listview_recompute_elements(lv);
  }
}

void listview_set_scroll_type(listview *lv, ScrollType type) {
  if (lv) {
    lv->scroll_type = type;
  }
}

void listview_set_mouse_activated_cb(listview *lv,
                                     listview_mouse_activated_cb cb,
                                     void *udata) {
  if (lv) {
    lv->mouse_activated = cb;
    lv->mouse_activated_data = udata;
  }
}
void listview_set_multi_select(listview *lv, gboolean enable) {
  if (lv) {
    lv->multi_select = enable;
  }
}
void listview_set_num_lines(listview *lv, unsigned int num_lines) {
  if (lv) {
    lv->menu_lines = num_lines;
  }
}

void listview_set_max_lines(listview *lv, unsigned int max_lines) {
  if (lv) {
    lv->max_displayed_lines = max_lines;
  }
}

gboolean listview_get_fixed_num_lines(listview *lv) {
  if (lv) {
    return lv->fixed_num_lines;
  }
  return FALSE;
}
void listview_set_fixed_num_lines(listview *lv) {
  if (lv) {
    lv->fixed_num_lines = TRUE;
  }
}

void listview_set_ellipsize_start(listview *lv) {
  if (lv) {
    lv->emode = PANGO_ELLIPSIZE_START;
    for (unsigned int i = 0; i < lv->cur_elements; i++) {
      textbox_set_ellipsize(lv->boxes[i].textbox, lv->emode);
    }
  }
}

void listview_toggle_ellipsizing(listview *lv) {
  if (lv) {
    PangoEllipsizeMode mode = lv->emode;
    if (mode == PANGO_ELLIPSIZE_START) {
      mode = PANGO_ELLIPSIZE_MIDDLE;
    } else if (mode == PANGO_ELLIPSIZE_MIDDLE) {
      mode = PANGO_ELLIPSIZE_END;
    } else if (mode == PANGO_ELLIPSIZE_END) {
      mode = PANGO_ELLIPSIZE_START;
    }
    lv->emode = mode;
    for (unsigned int i = 0; i < lv->cur_elements; i++) {
      textbox_set_ellipsize(lv->boxes[i].textbox, mode);
    }
  }
}
