]> git.ozlabs.org Git - petitboot/blobdiff - ui/twin/pb-twin.c
Initial support for multiple UIs
[petitboot] / ui / twin / pb-twin.c
diff --git a/ui/twin/pb-twin.c b/ui/twin/pb-twin.c
new file mode 100644 (file)
index 0000000..ee68930
--- /dev/null
@@ -0,0 +1,1194 @@
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <unistd.h>
+#include <syscall.h>
+#include <assert.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+
+#include <linux/input.h>
+
+#undef _USE_X11
+
+#include <libtwin/twin.h>
+#include <libtwin/twin_linux_mouse.h>
+#include <libtwin/twin_linux_js.h>
+#include <libtwin/twin_png.h>
+#include <libtwin/twin_jpeg.h>
+
+#include "petitboot.h"
+#include "petitboot-paths.h"
+
+#ifdef _USE_X11
+#include <libtwin/twin_x11.h>
+static twin_x11_t *pboot_x11;
+#else
+#include <libtwin/twin_fbdev.h>
+static twin_fbdev_t *pboot_fbdev;
+#endif
+
+static twin_screen_t *pboot_screen;
+
+#define PBOOT_INITIAL_MESSAGE          \
+       "keys: 0=safe 1=720p 2=1080i 3=1080p del=GameOS"
+
+#define PBOOT_LEFT_PANE_SIZE           160
+#define PBOOT_LEFT_PANE_COLOR          0x80000000
+#define PBOOT_LEFT_LINE_COLOR          0xff000000
+
+#define PBOOT_LEFT_FOCUS_WIDTH         80
+#define PBOOT_LEFT_FOCUS_HEIGHT                80
+#define PBOOT_LEFT_FOCUS_XOFF          40
+#define PBOOT_LEFT_FOCUS_YOFF          40
+#define PBOOT_LEFT_FOCUS_XRAD          (6 * TWIN_FIXED_ONE)
+#define PBOOT_LEFT_FOCUS_YRAD          (6 * TWIN_FIXED_ONE)
+
+#define PBOOT_RIGHT_FOCUS_XOFF         20
+#define PBOOT_RIGHT_FOCUS_YOFF         60
+#define PBOOT_RIGHT_FOCUS_HEIGHT       80
+#define PBOOT_RIGHT_FOCUS_XRAD         (6 * TWIN_FIXED_ONE)
+#define PBOOT_RIGHT_FOCUS_YRAD         (6 * TWIN_FIXED_ONE)
+
+#define PBOOT_LEFT_ICON_WIDTH          64
+#define PBOOT_LEFT_ICON_HEIGHT         64
+#define PBOOT_LEFT_ICON_XOFF           50
+#define PBOOT_LEFT_ICON_YOFF           50
+#define PBOOT_LEFT_ICON_STRIDE         100
+
+#define PBOOT_RIGHT_OPTION_LMARGIN     30
+#define PBOOT_RIGHT_OPTION_RMARGIN     30
+#define PBOOT_RIGHT_OPTION_TMARGIN     70
+#define PBOOT_RIGHT_OPTION_HEIGHT      64
+#define PBOOT_RIGHT_OPTION_STRIDE      100
+#define PBOOT_RIGHT_TITLE_TEXT_SIZE    (30 * TWIN_FIXED_ONE)
+#define PBOOT_RIGHT_SUBTITLE_TEXT_SIZE (18 * TWIN_FIXED_ONE)
+#define PBOOT_RIGHT_TITLE_XOFFSET      80
+#define PBOOT_RIGHT_TITLE_YOFFSET      30
+#define PBOOT_RIGHT_SUBTITLE_XOFFSET   100
+#define PBOOT_RIGHT_SUBTITLE_YOFFSET   50
+#define PBOOT_RIGHT_BADGE_XOFFSET      2
+#define PBOOT_RIGHT_BADGE_YOFFSET      0
+
+
+#define PBOOT_RIGHT_TITLE_COLOR                0xff000000
+#define PBOOT_RIGHT_SUBTITLE_COLOR     0xff400000
+
+#define PBOOT_FOCUS_COLOR              0x10404040
+
+#define PBOOT_STATUS_PANE_COLOR                0x60606060
+#define PBOOT_STATUS_PANE_HEIGHT       20
+#define PBOOT_STATUS_PANE_XYMARGIN     20
+#define PBOOT_STATUS_TEXT_MARGIN       10
+#define PBOOT_STATUS_TEXT_SIZE         (16 * TWIN_FIXED_ONE)
+#define PBOOT_STATUS_TEXT_COLOR                0xff000000
+
+typedef struct _pboot_option pboot_option_t;
+typedef struct _pboot_device pboot_device_t;
+
+struct _pboot_option
+{
+       char            *title;
+       char            *subtitle;
+       twin_pixmap_t   *badge;
+       twin_pixmap_t   *cache;
+       twin_rect_t     box;
+       void            *data;
+};
+
+struct _pboot_device
+{
+       char                    *id;
+       twin_pixmap_t           *badge;
+       twin_rect_t             box;
+       int                     option_count;
+       pboot_option_t          options[PBOOT_MAX_OPTION];
+};
+
+static twin_pixmap_t   *pboot_cursor;
+static int             pboot_cursor_hx;
+static int             pboot_cursor_hy;
+
+static pboot_device_t  *pboot_devices[PBOOT_MAX_DEV];
+static int             pboot_dev_count;
+static int             pboot_dev_sel = -1;
+static int             pboot_focus_lpane = 1;
+
+typedef struct _pboot_lpane {
+       twin_window_t   *window;
+       twin_rect_t     focus_box;
+       int             focus_start;
+       int             focus_target;
+       int             focus_curindex;
+       int             mouse_target;
+} pboot_lpane_t;
+
+typedef struct _pboot_rpane {
+       twin_window_t   *window;
+       twin_rect_t     focus_box;
+       int             focus_start;
+       int             focus_target;
+       int             focus_curindex;
+       int             mouse_target;
+} pboot_rpane_t;
+
+typedef struct _pboot_spane {
+       twin_window_t   *window;
+       char            *text;
+} pboot_spane_t;
+
+static pboot_lpane_t   *pboot_lpane;
+static pboot_rpane_t   *pboot_rpane;
+static pboot_spane_t   *pboot_spane;
+
+/* control to keyboard mappings for the sixaxis controller */
+uint8_t sixaxis_map[] = {
+       0,              /*   0  Select          */
+       0,              /*   1  L3              */
+       0,              /*   2  R3              */
+       0,              /*   3  Start           */
+       KEY_UP,         /*   4  Dpad Up         */
+       KEY_RIGHT,      /*   5  Dpad Right      */
+       KEY_DOWN,       /*   6  Dpad Down       */
+       KEY_LEFT,       /*   7  Dpad Left       */
+       0,              /*   8  L2              */
+       0,              /*   9  R2              */
+       0,              /*  10  L1              */
+       0,              /*  11  R1              */
+       0,              /*  12  Triangle        */
+       KEY_ENTER,      /*  13  Circle          */
+       0,              /*  14  Cross           */
+       KEY_DELETE,     /*  15  Square          */
+       0,              /*  16  PS Button       */
+       0,              /*  17  nothing      */
+       0,              /*  18  nothing      */
+};
+
+
+static int pboot_vmode_change = -1;
+
+/* XXX move to twin */
+static inline twin_bool_t twin_rect_intersect(twin_rect_t r1,
+                                             twin_rect_t r2)
+{
+       return !(r1.left > r2.right ||
+                r1.right < r2.left ||
+                r1.top > r2.bottom ||
+                r1.bottom < r2.top);
+}
+
+static void pboot_draw_option_cache(pboot_device_t *dev, pboot_option_t *opt,
+                                   int index)
+{
+       twin_pixmap_t   *px;
+       twin_path_t     *path;
+       twin_fixed_t    tx, ty;
+
+       /* Create pixmap */
+       px = twin_pixmap_create(TWIN_ARGB32, opt->box.right - opt->box.left,
+                                opt->box.bottom - opt->box.top);
+       assert(px);
+       opt->cache = px;
+
+       /* Fill background */
+       twin_fill(px, 0x00000000, TWIN_SOURCE, 0, 0, px->width, px->height);
+
+       /* Allocate a path for drawing */
+       path = twin_path_create();
+       assert(path);
+
+#if 0
+       /* TEST - Bounding rectangle */
+       twin_path_rectangle(path, 0, 0,
+                           twin_int_to_fixed(px->width),
+                           twin_int_to_fixed(px->height));
+       twin_paint_path(px, PBOOT_RIGHT_TITLE_COLOR, path);
+       twin_path_empty(path);
+       twin_fill(px, 0x00000000, TWIN_SOURCE, 2, 2,
+                 px->width - 3, px->height - 3);
+#endif
+
+       /* Draw texts */
+       twin_path_set_font_size(path, PBOOT_RIGHT_TITLE_TEXT_SIZE);
+       twin_path_set_font_style(path, TWIN_TEXT_UNHINTED);
+       tx = twin_int_to_fixed(PBOOT_RIGHT_TITLE_XOFFSET);
+       ty = twin_int_to_fixed(PBOOT_RIGHT_TITLE_YOFFSET);
+       twin_path_move (path, tx, ty);
+       twin_path_utf8 (path, opt->title);
+       twin_paint_path (px, PBOOT_RIGHT_TITLE_COLOR, path);
+       twin_path_empty (path);
+
+       if (opt->subtitle) {
+               twin_path_set_font_size(path, PBOOT_RIGHT_SUBTITLE_TEXT_SIZE);
+               twin_path_set_font_style(path, TWIN_TEXT_UNHINTED);
+               tx = twin_int_to_fixed(PBOOT_RIGHT_SUBTITLE_XOFFSET);
+               ty = twin_int_to_fixed(PBOOT_RIGHT_SUBTITLE_YOFFSET);
+               twin_path_move (path, tx, ty);
+               twin_path_utf8 (path, opt->subtitle);
+               twin_paint_path (px, PBOOT_RIGHT_SUBTITLE_COLOR, path);
+               twin_path_empty (path);
+       }
+
+       if (opt->badge) {
+               twin_operand_t  src;
+
+               src.source_kind = TWIN_PIXMAP;
+               src.u.pixmap = opt->badge;
+
+               twin_composite(px, PBOOT_RIGHT_BADGE_XOFFSET,
+                              PBOOT_RIGHT_BADGE_YOFFSET,
+                              &src, 0, 0, NULL, 0, 0, TWIN_OVER,
+                              opt->badge->width, opt->badge->height);
+       }
+
+
+       /* Destroy path */
+       twin_path_destroy(path);
+}
+
+static void pboot_rpane_draw(twin_window_t *window)
+{
+       twin_pixmap_t   *px = window->pixmap;
+       pboot_rpane_t   *rpane = window->client_data;
+       pboot_device_t  *dev;
+       twin_path_t     *path;
+       twin_fixed_t    x, y, w, h;
+       int             i;
+
+       /* Fill background */
+       twin_fill(px, 0x00000000, TWIN_SOURCE, 0, 0, px->width, px->height);
+
+       /* Nothing to draw, return */
+       if (pboot_dev_sel < 0)
+               return;
+
+       /* Create a path for use later */
+       path = twin_path_create();
+       assert(path);
+
+       /* Draw focus box */
+       if (rpane->focus_curindex >= 0 &&
+           twin_rect_intersect(rpane->focus_box, px->clip)) {
+               x = twin_int_to_fixed(rpane->focus_box.left + 2);
+               y = twin_int_to_fixed(rpane->focus_box.top + 2);
+               w = twin_int_to_fixed(rpane->focus_box.right -
+                                     rpane->focus_box.left - 4);
+               h = twin_int_to_fixed(rpane->focus_box.bottom -
+                                     rpane->focus_box.top - 4);
+               twin_path_rounded_rectangle(path, x, y, w, h,
+                                           PBOOT_RIGHT_FOCUS_XRAD,
+                                           PBOOT_RIGHT_FOCUS_YRAD);
+               if (!pboot_focus_lpane)
+                       twin_paint_path(px, PBOOT_FOCUS_COLOR, path);
+               else
+                       twin_paint_stroke(px, PBOOT_FOCUS_COLOR, path,
+                                         4 * TWIN_FIXED_ONE);
+       }
+
+       /* Get device and iterate through options */
+       dev = pboot_devices[pboot_dev_sel];
+       for (i = 0; i < dev->option_count; i++) {
+               pboot_option_t  *opt = &dev->options[i];
+               twin_operand_t  src;
+
+               if (opt->title == NULL)
+                       continue;
+               if (!twin_rect_intersect(opt->box, px->clip))
+                       continue;
+               if (opt->cache == NULL)
+                       pboot_draw_option_cache(dev, opt, i);
+
+               src.source_kind = TWIN_PIXMAP;
+               src.u.pixmap = opt->cache;
+
+               twin_composite(px, opt->box.left, opt->box.top,
+                              &src, 0, 0, NULL, 0, 0, TWIN_OVER,
+                              opt->box.right - opt->box.left,
+                              opt->box.bottom - opt->box.top);
+       }
+
+       /* Destroy path */
+       twin_path_destroy(path);
+}
+
+static twin_time_t pboot_rfocus_timeout (twin_time_t now, void *closure)
+{
+       int dir = 1, dist, pos;
+       const int accel[11] = { 7, 4, 2, 1, 1, 1, 1, 1, 2, 2, 3 };
+
+       dist = abs(pboot_rpane->focus_target - pboot_rpane->focus_start);
+       dir = dist > 5 ? 5 : dist;
+       pos = pboot_rpane->focus_target - (int)pboot_rpane->focus_box.top;
+       if (pos == 0) {
+               return -1;
+       }
+       if (pos < 0) {
+               dir = -dir;
+               pos = -pos;
+       }
+       twin_window_damage(pboot_rpane->window,
+                          pboot_rpane->focus_box.left,
+                          pboot_rpane->focus_box.top,
+                          pboot_rpane->focus_box.right,
+                          pboot_rpane->focus_box.bottom);
+
+       pboot_rpane->focus_box.top += dir;
+       pboot_rpane->focus_box.bottom += dir;
+
+       twin_window_damage(pboot_rpane->window,
+                          pboot_rpane->focus_box.left,
+                          pboot_rpane->focus_box.top,
+                          pboot_rpane->focus_box.right,
+                          pboot_rpane->focus_box.bottom);
+
+       twin_window_queue_paint(pboot_rpane->window);
+
+       return accel[(pos * 10) / dist];
+}
+
+static void pboot_set_rfocus(int index)
+{
+       pboot_device_t  *dev;
+
+       if (pboot_dev_sel < 0 || pboot_dev_sel >= pboot_dev_count)
+               return;
+       dev = pboot_devices[pboot_dev_sel];
+       if (index < 0 || index >= dev->option_count)
+               return;
+
+       pboot_rpane->focus_start = pboot_rpane->focus_box.top;
+       pboot_rpane->focus_target = PBOOT_RIGHT_FOCUS_YOFF +
+               PBOOT_RIGHT_OPTION_STRIDE * index;
+       pboot_rpane->focus_curindex = index;
+
+       twin_set_timeout(pboot_rfocus_timeout, 0, NULL);
+}
+
+static void pboot_select_rpane(void)
+{
+       if (pboot_focus_lpane == 0)
+               return;
+       pboot_focus_lpane = 0;
+
+       twin_screen_set_active(pboot_screen, pboot_rpane->window->pixmap);
+
+       twin_window_damage(pboot_lpane->window,
+                          pboot_lpane->focus_box.left,
+                          pboot_lpane->focus_box.top,
+                          pboot_lpane->focus_box.right,
+                          pboot_lpane->focus_box.bottom);
+
+       twin_window_damage(pboot_rpane->window,
+                          pboot_rpane->focus_box.left,
+                          pboot_rpane->focus_box.top,
+                          pboot_rpane->focus_box.right,
+                          pboot_rpane->focus_box.bottom);
+
+       twin_window_queue_paint(pboot_lpane->window);
+       twin_window_queue_paint(pboot_rpane->window);
+
+       pboot_set_rfocus(0);
+}
+
+static void pboot_select_lpane(void)
+{
+       if (pboot_focus_lpane == 1)
+               return;
+       pboot_focus_lpane = 1;
+
+       twin_screen_set_active(pboot_screen, pboot_lpane->window->pixmap);
+
+       twin_window_damage(pboot_lpane->window,
+                          pboot_lpane->focus_box.left,
+                          pboot_lpane->focus_box.top,
+                          pboot_lpane->focus_box.right,
+                          pboot_lpane->focus_box.bottom);
+
+       twin_window_damage(pboot_rpane->window,
+                          pboot_rpane->focus_box.left,
+                          pboot_rpane->focus_box.top,
+                          pboot_rpane->focus_box.right,
+                          pboot_rpane->focus_box.bottom);
+
+       twin_window_queue_paint(pboot_lpane->window);
+       twin_window_queue_paint(pboot_rpane->window);
+}
+
+static void pboot_rpane_mousetrack(twin_coord_t x, twin_coord_t y)
+{
+       pboot_device_t  *dev;
+       pboot_option_t  *opt;
+       int             candidate = -1;
+
+       if (pboot_dev_sel < 0 || pboot_dev_sel >= pboot_dev_count)
+               return;
+       dev = pboot_devices[pboot_dev_sel];
+
+       if (y < PBOOT_RIGHT_OPTION_TMARGIN)
+               goto miss;
+       candidate = (y - PBOOT_RIGHT_OPTION_TMARGIN) /
+               PBOOT_RIGHT_OPTION_STRIDE;
+       if (candidate >= dev->option_count) {
+               candidate = -1;
+               goto miss;
+       }
+       if (candidate == pboot_rpane->mouse_target)
+               return;
+       opt = &dev->options[candidate];
+       if (x < opt->box.left || x > opt->box.right ||
+           y < opt->box.top || y > opt->box.bottom) {
+               candidate = -1;
+               goto miss;
+       }
+
+       /* Ok, so now, we know the mouse hit an icon that wasn't the same
+        * as the previous one, we trigger a focus change
+        */
+       pboot_set_rfocus(candidate);
+
+ miss:
+       pboot_rpane->mouse_target = candidate;
+}
+
+static void pboot_choose_option(void)
+{
+       pboot_device_t *dev = pboot_devices[pboot_dev_sel];
+       pboot_option_t *opt = &dev->options[pboot_rpane->focus_curindex];
+
+       LOG("Selected device %s\n", opt->title);
+       pboot_message("booting %s...", opt->title);
+
+       /* Give user feedback, make sure errors and panics will be seen */
+       pboot_exec_option(opt->data);
+}
+
+static twin_bool_t pboot_rpane_event (twin_window_t        *window,
+                                     twin_event_t          *event)
+{
+       /* filter out all mouse events */
+       switch(event->kind) {
+       case TwinEventEnter:
+       case TwinEventMotion:
+       case TwinEventLeave:
+               pboot_select_rpane();
+               pboot_rpane_mousetrack(event->u.pointer.x, event->u.pointer.y);
+               return TWIN_TRUE;
+       case TwinEventButtonDown:
+               pboot_select_rpane();
+               pboot_rpane_mousetrack(event->u.pointer.x, event->u.pointer.y);
+               pboot_choose_option();
+       case TwinEventButtonUp:
+               return TWIN_TRUE;
+       case TwinEventKeyDown:
+               switch(event->u.key.key) {
+               case KEY_UP:
+                       pboot_set_rfocus(pboot_rpane->focus_curindex - 1);
+                       return TWIN_TRUE;
+               case KEY_DOWN:
+                       pboot_set_rfocus(pboot_rpane->focus_curindex + 1);
+                       return TWIN_TRUE;
+               case KEY_LEFT:
+                       pboot_select_lpane();
+                       return TWIN_TRUE;
+               case KEY_ENTER:
+                       pboot_choose_option();
+               default:
+                       break;
+               }
+               break;
+       default:
+               break;
+       }
+       return TWIN_FALSE;
+}
+
+
+int pboot_add_option(int devindex, const char *title,
+                    const char *subtitle, twin_pixmap_t *badge, void *data)
+{
+       pboot_device_t  *dev;
+       pboot_option_t  *opt;
+       twin_coord_t    width;
+       int             index;
+
+       if (devindex < 0 || devindex >= pboot_dev_count)
+               return -1;
+       dev = pboot_devices[devindex];
+
+       if (dev->option_count >= PBOOT_MAX_OPTION)
+               return -1;
+       index = dev->option_count++;
+       opt = &dev->options[index];
+
+       opt->title = malloc(strlen(title) + 1);
+       strcpy(opt->title, title);
+
+       if (subtitle) {
+               opt->subtitle = malloc(strlen(subtitle) + 1);
+               strcpy(opt->subtitle, subtitle);
+       } else
+               opt->subtitle = NULL;
+
+       opt->badge = badge;
+       opt->cache = NULL;
+
+       width = pboot_rpane->window->pixmap->width -
+               (PBOOT_RIGHT_OPTION_LMARGIN + PBOOT_RIGHT_OPTION_RMARGIN);
+
+       opt->box.left = PBOOT_RIGHT_OPTION_LMARGIN;
+       opt->box.right = opt->box.left + width;
+       opt->box.top = PBOOT_RIGHT_OPTION_TMARGIN +
+               index * PBOOT_RIGHT_OPTION_STRIDE;
+       opt->box.bottom = opt->box.top + PBOOT_RIGHT_OPTION_HEIGHT;
+
+       opt->data = data;
+       return index;
+}
+
+
+static void pboot_set_device_select(int sel, int force)
+{
+       LOG("%s: %d -> %d\n", __FUNCTION__, pboot_dev_sel, sel);
+       if (!force && sel == pboot_dev_sel)
+               return;
+       if (sel >= pboot_dev_count)
+               return;
+       pboot_dev_sel = sel;
+       if (force) {
+               pboot_lpane->focus_curindex = sel;
+               if (sel < 0)
+                       pboot_lpane->focus_target = 0 - PBOOT_LEFT_FOCUS_HEIGHT;
+               else
+                       pboot_lpane->focus_target = PBOOT_LEFT_FOCUS_YOFF +
+                               PBOOT_LEFT_ICON_STRIDE * sel;
+               pboot_rpane->focus_box.bottom = pboot_lpane->focus_target;
+               pboot_rpane->focus_box.bottom = pboot_rpane->focus_box.top +
+                       PBOOT_RIGHT_FOCUS_HEIGHT;
+               twin_window_damage(pboot_lpane->window,
+                                  0, 0,
+                                  pboot_lpane->window->pixmap->width,
+                                  pboot_lpane->window->pixmap->height);
+               twin_window_queue_paint(pboot_lpane->window);
+       }
+       pboot_rpane->focus_curindex = -1;
+       pboot_rpane->mouse_target = -1;
+       pboot_rpane->focus_box.top = -2*PBOOT_RIGHT_FOCUS_HEIGHT;
+       pboot_rpane->focus_box.bottom = pboot_rpane->focus_box.top +
+               PBOOT_RIGHT_FOCUS_HEIGHT;
+       twin_window_damage(pboot_rpane->window, 0, 0,
+                          pboot_rpane->window->pixmap->width,
+                          pboot_rpane->window->pixmap->height);
+       twin_window_queue_paint(pboot_rpane->window);
+}
+
+static void pboot_create_rpane(void)
+{
+       pboot_rpane = calloc(1, sizeof(pboot_rpane_t));
+       assert(pboot_rpane);
+
+       pboot_rpane->window = twin_window_create(pboot_screen, TWIN_ARGB32,
+                                                TwinWindowPlain,
+                                                PBOOT_LEFT_PANE_SIZE, 0,
+                                                pboot_screen->width -
+                                                  PBOOT_LEFT_PANE_SIZE,
+                                                pboot_screen->height);
+       assert(pboot_rpane->window);
+
+       pboot_rpane->window->draw = pboot_rpane_draw;
+       pboot_rpane->window->event = pboot_rpane_event;
+       pboot_rpane->window->client_data = pboot_rpane;
+
+       pboot_rpane->focus_curindex = -1;
+       pboot_rpane->focus_box.left = PBOOT_RIGHT_FOCUS_XOFF;
+       pboot_rpane->focus_box.top = -2*PBOOT_RIGHT_FOCUS_HEIGHT;
+       pboot_rpane->focus_box.right = pboot_rpane->window->pixmap->width -
+               2 * PBOOT_RIGHT_FOCUS_XOFF;
+       pboot_rpane->focus_box.bottom = pboot_rpane->focus_box.top +
+               PBOOT_RIGHT_FOCUS_HEIGHT;
+       pboot_rpane->mouse_target = -1;
+       twin_window_show(pboot_rpane->window);
+       twin_window_queue_paint(pboot_rpane->window);
+}
+
+
+static twin_time_t pboot_lfocus_timeout (twin_time_t now, void *closure)
+{
+       int dir = 1, dist, pos;
+       const int accel[11] = { 7, 4, 2, 1, 1, 1, 1, 1, 2, 2, 3 };
+
+       dist = abs(pboot_lpane->focus_target - pboot_lpane->focus_start);
+       dir = dist > 2 ? 2 : dist;
+       pos = pboot_lpane->focus_target - (int)pboot_lpane->focus_box.top;
+       if (pos == 0) {
+               pboot_set_device_select(pboot_lpane->focus_curindex, 0);
+               return -1;
+       }
+       if (pos < 0) {
+               dir = -1;
+               pos = -pos;
+       }
+       twin_window_damage(pboot_lpane->window,
+                          pboot_lpane->focus_box.left,
+                          pboot_lpane->focus_box.top,
+                          pboot_lpane->focus_box.right,
+                          pboot_lpane->focus_box.bottom);
+
+       pboot_lpane->focus_box.top += dir;
+       pboot_lpane->focus_box.bottom += dir;
+
+       twin_window_damage(pboot_lpane->window,
+                          pboot_lpane->focus_box.left,
+                          pboot_lpane->focus_box.top,
+                          pboot_lpane->focus_box.right,
+                          pboot_lpane->focus_box.bottom);
+
+       twin_window_queue_paint(pboot_lpane->window);
+
+       return accel[(pos * 10) / dist];
+}
+
+static void pboot_set_lfocus(int index)
+{
+       if (index >= pboot_dev_count)
+               return;
+
+       pboot_lpane->focus_start = pboot_lpane->focus_box.top;
+
+       if (index < 0)
+               pboot_lpane->focus_target = 0 - PBOOT_LEFT_FOCUS_HEIGHT;
+       else
+               pboot_lpane->focus_target = PBOOT_LEFT_FOCUS_YOFF +
+                       PBOOT_LEFT_ICON_STRIDE * index;
+
+       pboot_lpane->focus_curindex = index;
+
+       twin_set_timeout(pboot_lfocus_timeout, 0, NULL);
+}
+
+static void pboot_lpane_mousetrack(twin_coord_t x, twin_coord_t y)
+{
+       int candidate = -1;
+       twin_coord_t icon_top;
+
+       if (x < PBOOT_LEFT_ICON_XOFF ||
+           x > (PBOOT_LEFT_ICON_XOFF + PBOOT_LEFT_ICON_WIDTH))
+               goto miss;
+       if (y < PBOOT_LEFT_ICON_YOFF)
+               goto miss;
+       candidate = (y - PBOOT_LEFT_ICON_YOFF) / PBOOT_LEFT_ICON_STRIDE;
+       if (candidate >= pboot_dev_count) {
+               candidate = -1;
+               goto miss;
+       }
+       if (candidate == pboot_lpane->mouse_target)
+               return;
+       icon_top = PBOOT_LEFT_ICON_YOFF +
+               candidate * PBOOT_LEFT_ICON_STRIDE;
+       if (y > (icon_top + PBOOT_LEFT_ICON_HEIGHT)) {
+               candidate = -1;
+               goto miss;
+       }
+
+       /* Ok, so now, we know the mouse hit an icon that wasn't the same
+        * as the previous one, we trigger a focus change
+        */
+       pboot_set_lfocus(candidate);
+
+ miss:
+       pboot_lpane->mouse_target = candidate;
+}
+
+static twin_bool_t pboot_lpane_event (twin_window_t        *window,
+                                     twin_event_t          *event)
+{
+       /* filter out all mouse events */
+       switch(event->kind) {
+       case TwinEventEnter:
+       case TwinEventMotion:
+       case TwinEventLeave:
+               pboot_select_lpane();
+               pboot_lpane_mousetrack(event->u.pointer.x, event->u.pointer.y);
+               return TWIN_TRUE;
+       case TwinEventButtonDown:
+       case TwinEventButtonUp:
+               return TWIN_TRUE;
+       case TwinEventKeyDown:
+               switch(event->u.key.key) {
+               case KEY_UP:
+                       if (pboot_lpane->focus_curindex > 0)
+                               pboot_set_lfocus(
+                                       pboot_lpane->focus_curindex - 1);
+                       return TWIN_TRUE;
+               case KEY_DOWN:
+                       pboot_set_lfocus(pboot_lpane->focus_curindex + 1);
+                       return TWIN_TRUE;
+               case KEY_RIGHT:
+                       pboot_select_rpane();
+                       return TWIN_TRUE;
+               default:
+                       break;
+               }
+               break;
+       default:
+               break;
+       }
+       return TWIN_FALSE;
+}
+
+static void pboot_quit(void)
+{
+       kill(0, SIGINT);
+}
+
+twin_bool_t pboot_event_filter(twin_screen_t       *screen,
+                              twin_event_t         *event)
+{
+       switch(event->kind) {
+       case TwinEventEnter:
+       case TwinEventMotion:
+       case TwinEventLeave:
+       case TwinEventButtonDown:
+       case TwinEventButtonUp:
+               if (pboot_cursor != NULL)
+                       twin_screen_set_cursor(pboot_screen, pboot_cursor,
+                                              pboot_cursor_hx,
+                                              pboot_cursor_hy);
+               break;
+       case TwinEventJoyButton:
+               /* map joystick events into key events */
+               if (event->u.js.control >= sizeof(sixaxis_map))
+                       break;
+
+               event->u.key.key = sixaxis_map[event->u.js.control];
+               if (event->u.js.value == 0) {
+                       event->kind = TwinEventKeyUp;
+                       break;
+               } else {
+                       event->kind = TwinEventKeyDown;
+               }
+
+               /* fall through.. */
+       case TwinEventKeyDown:
+               switch(event->u.key.key) {
+               /* Gross hack for video modes, need something better ! */
+               case KEY_0:
+                       pboot_vmode_change = 0; /* auto */
+                       pboot_quit();
+                       return TWIN_TRUE;
+               case KEY_1:
+                       pboot_vmode_change = 3; /* 720p */
+                       pboot_quit();
+                       return TWIN_TRUE;
+               case KEY_2:
+                       pboot_vmode_change = 4; /* 1080i */
+                       pboot_quit();
+                       return TWIN_TRUE;
+               case KEY_3:
+                       pboot_vmode_change = 5; /* 1080p */
+                       pboot_quit();
+                       return TWIN_TRUE;
+
+               /* Another gross hack for booting back to gameos */
+               case KEY_BACKSPACE:
+               case KEY_DELETE:
+                       pboot_message("booting to GameOS...");
+                       system(BOOT_GAMEOS_BIN);
+               }
+       case TwinEventKeyUp:
+               twin_screen_set_cursor(pboot_screen, NULL, 0, 0);
+               break;
+       default:
+               break;
+       }
+       return TWIN_FALSE;
+}
+
+static void pboot_lpane_draw(twin_window_t *window)
+{
+       twin_pixmap_t   *px = window->pixmap;
+       pboot_lpane_t   *lpane = window->client_data;
+       twin_path_t     *path;
+       twin_fixed_t    x, y, w, h;
+       int             i;
+
+       /* Fill background */
+       twin_fill(px, PBOOT_LEFT_PANE_COLOR, TWIN_SOURCE,
+                 0, 0, px->width, px->height);
+
+       /* Create a path for use later */
+       path = twin_path_create();
+       assert(path);
+
+       /* Draw right line if needed */
+       if (px->clip.right > (PBOOT_LEFT_PANE_SIZE - 4)) {
+               x = twin_int_to_fixed(PBOOT_LEFT_PANE_SIZE - 4);
+               y = twin_int_to_fixed(px->height);
+               twin_path_rectangle(path, x, 0, 0x40000, y);
+               twin_paint_path(px, PBOOT_LEFT_LINE_COLOR, path);
+               twin_path_empty(path);
+       }
+
+       /* Draw focus box */
+       if (lpane->focus_curindex >= 0 &&
+           twin_rect_intersect(lpane->focus_box, px->clip)) {
+               x = twin_int_to_fixed(lpane->focus_box.left + 2);
+               y = twin_int_to_fixed(lpane->focus_box.top + 2);
+               w = twin_int_to_fixed(lpane->focus_box.right -
+                                     lpane->focus_box.left - 4);
+               h = twin_int_to_fixed(lpane->focus_box.bottom -
+                                     lpane->focus_box.top - 4);
+               twin_path_rounded_rectangle(path, x, y, w, h,
+                                           PBOOT_LEFT_FOCUS_XRAD,
+                                           PBOOT_LEFT_FOCUS_YRAD);
+               if (pboot_focus_lpane)
+                       twin_paint_path(px, PBOOT_FOCUS_COLOR, path);
+               else
+                       twin_paint_stroke(px, PBOOT_FOCUS_COLOR, path,
+                                         4 * TWIN_FIXED_ONE);
+       }
+
+       /* Draw icons */
+       for (i = 0; i < pboot_dev_count; i++) {
+               pboot_device_t  *dev = pboot_devices[i];
+               twin_operand_t  src;
+
+               if (!twin_rect_intersect(dev->box, px->clip))
+                       continue;
+
+               src.source_kind = TWIN_PIXMAP;
+               src.u.pixmap = dev->badge;
+
+               twin_composite(px, dev->box.left, dev->box.top,
+                              &src, 0, 0, NULL, 0, 0, TWIN_OVER,
+                              dev->box.right - dev->box.left,
+                              dev->box.bottom - dev->box.top);
+
+       }
+
+       /* Destroy path */
+       twin_path_destroy(path);
+}
+
+static void pboot_create_lpane(void)
+{
+       pboot_lpane = calloc(1, sizeof(pboot_lpane_t));
+       assert(pboot_lpane);
+
+       pboot_lpane->window = twin_window_create(pboot_screen, TWIN_ARGB32,
+                                                TwinWindowPlain,
+                                                0, 0, PBOOT_LEFT_PANE_SIZE,
+                                                pboot_screen->height);
+       assert(pboot_lpane->window);
+
+       pboot_lpane->window->draw = pboot_lpane_draw;
+       pboot_lpane->window->event = pboot_lpane_event;
+       pboot_lpane->window->client_data = pboot_lpane;
+       pboot_lpane->focus_curindex = -1;
+       pboot_lpane->focus_box.left = PBOOT_LEFT_FOCUS_XOFF;
+       pboot_lpane->focus_box.top = -2*PBOOT_LEFT_FOCUS_HEIGHT;
+       pboot_lpane->focus_box.right = pboot_lpane->focus_box.left +
+               PBOOT_LEFT_FOCUS_WIDTH;
+       pboot_lpane->focus_box.bottom = pboot_lpane->focus_box.top +
+               PBOOT_LEFT_FOCUS_HEIGHT;
+       pboot_lpane->mouse_target = -1;
+       twin_window_show(pboot_lpane->window);
+       twin_window_queue_paint(pboot_lpane->window);
+}
+
+static void pboot_spane_draw(twin_window_t *window)
+{
+       twin_pixmap_t   *px = window->pixmap;
+       pboot_spane_t   *spane = window->client_data;
+       twin_path_t     *path;
+       twin_fixed_t    tx, ty;
+
+       /* Fill background */
+       twin_fill(px, PBOOT_STATUS_PANE_COLOR, TWIN_SOURCE,
+                 0, 0, px->width, px->height);
+
+       path = twin_path_create();
+       assert(path);
+
+       twin_path_set_font_size(path, PBOOT_STATUS_TEXT_SIZE);
+       twin_path_set_font_style(path, TWIN_TEXT_UNHINTED);
+       tx = twin_int_to_fixed(PBOOT_STATUS_TEXT_MARGIN);
+       ty = twin_int_to_fixed(PBOOT_STATUS_PANE_HEIGHT - 2);
+       twin_path_move (path, tx, ty);
+       twin_path_utf8 (path, spane->text);
+       twin_paint_path (px, PBOOT_STATUS_TEXT_COLOR, path);
+
+       twin_path_destroy(path);
+}
+
+void pboot_message(const char *fmt, ...)
+{
+       va_list ap;
+       char *msg;
+
+       if (pboot_spane->text)
+               free(pboot_spane->text);
+
+       va_start(ap, fmt);
+       vasprintf(&msg, fmt, ap);
+       va_end(ap);
+
+       pboot_spane->text = msg;
+       twin_window_damage(pboot_spane->window,
+                          0, 0,
+                          pboot_spane->window->pixmap->width,
+                          pboot_spane->window->pixmap->height);
+       twin_window_draw(pboot_spane->window);
+}
+
+static void pboot_create_spane(void)
+{
+       pboot_spane = calloc(1, sizeof(pboot_spane_t));
+       assert(pboot_spane);
+
+       pboot_spane->window = twin_window_create(pboot_screen, TWIN_ARGB32,
+                                                TwinWindowPlain,
+                                                PBOOT_LEFT_PANE_SIZE +
+                                                 PBOOT_STATUS_PANE_XYMARGIN,
+                                                pboot_screen->height - 
+                                                 PBOOT_STATUS_PANE_HEIGHT,
+                                                pboot_screen->width -
+                                                 PBOOT_LEFT_PANE_SIZE -
+                                                 2*PBOOT_STATUS_PANE_XYMARGIN,
+                                                PBOOT_STATUS_PANE_HEIGHT);
+       assert(pboot_spane->window);
+
+       pboot_spane->window->draw = pboot_spane_draw;
+       pboot_spane->window->client_data = pboot_spane;
+       pboot_spane->text = strdup(PBOOT_INITIAL_MESSAGE);
+       twin_window_show(pboot_spane->window);
+       twin_window_queue_paint(pboot_spane->window);
+}
+
+int pboot_add_device(const char *dev_id, const char *name,
+               twin_pixmap_t *pixmap)
+{
+       int             index;
+       pboot_device_t  *dev;
+
+       if (pboot_dev_count >= PBOOT_MAX_DEV)
+               return -1;
+
+       index = pboot_dev_count++;
+
+       dev = malloc(sizeof(*dev));
+       memset(dev, 0, sizeof(*dev));
+       dev->id = malloc(strlen(dev_id) + 1);
+       strcpy(dev->id, dev_id);
+       dev->badge = pixmap;
+       dev->box.left = PBOOT_LEFT_ICON_XOFF;
+       dev->box.right = dev->box.left + PBOOT_LEFT_ICON_WIDTH;
+       dev->box.top = PBOOT_LEFT_ICON_YOFF +
+               PBOOT_LEFT_ICON_STRIDE * index;
+       dev->box.bottom = dev->box.top + PBOOT_LEFT_ICON_HEIGHT;
+
+       pboot_devices[index] = dev;
+
+       twin_window_damage(pboot_lpane->window,
+                          dev->box.left, dev->box.top,
+                          dev->box.right, dev->box.bottom);
+       twin_window_queue_paint(pboot_lpane->window);
+
+       return index;
+}
+
+int pboot_remove_device(const char *dev_id)
+{
+       pboot_device_t  *dev = NULL;
+       int             i, newsel = pboot_dev_sel;
+
+       /* find the matching device */
+       for (i = 0; i < pboot_dev_count; i++) {
+               if (!strcmp(pboot_devices[i]->id, dev_id)) {
+                       dev = pboot_devices[i];
+                       break;
+               }
+       }
+
+       if (!dev)
+               return TWIN_FALSE;
+
+       memmove(pboot_devices + i, pboot_devices + i + 1,
+                       sizeof(*pboot_devices) * (pboot_dev_count + i - 1));
+       pboot_devices[--pboot_dev_count] = NULL;
+
+       /* select the newly-focussed device */
+       if (pboot_dev_sel > i)
+               newsel = pboot_dev_sel - 1;
+       else if (pboot_dev_sel == i && i >= pboot_dev_count)
+                       newsel = pboot_dev_count - 1;
+       pboot_set_device_select(newsel, 1);
+
+       /* todo: free device & options */
+
+       return TWIN_TRUE;
+}
+
+static void pboot_make_background(void)
+{
+       twin_pixmap_t   *filepic, *scaledpic;
+       const char      *background_path;
+
+       /* Set background pixmap */
+       LOG("loading background...");
+       background_path = artwork_pathname("background.jpg");
+       filepic = twin_jpeg_to_pixmap(background_path, TWIN_ARGB32);
+       LOG("%s\n", filepic ? "ok" : "failed");
+
+       if (filepic == NULL)
+               return;
+
+       if (pboot_screen->height == filepic->height &&
+           pboot_screen->width == filepic->width)
+               scaledpic = filepic;
+       else {
+               twin_fixed_t    sx, sy;
+               twin_operand_t  srcop;
+
+               scaledpic = twin_pixmap_create(TWIN_ARGB32,
+                                              pboot_screen->width,
+                                              pboot_screen->height);
+               if (scaledpic == NULL) {
+                       twin_pixmap_destroy(filepic);
+                       return;
+               }
+               sx = twin_fixed_div(twin_int_to_fixed(filepic->width),
+                                   twin_int_to_fixed(pboot_screen->width));
+               sy = twin_fixed_div(twin_int_to_fixed(filepic->height),
+                                   twin_int_to_fixed(pboot_screen->height));
+               
+               twin_matrix_scale(&filepic->transform, sx, sy);
+               srcop.source_kind = TWIN_PIXMAP;
+               srcop.u.pixmap = filepic;
+               twin_composite(scaledpic, 0, 0, &srcop, 0, 0,
+                              NULL, 0, 0, TWIN_SOURCE,
+                              pboot_screen->width, pboot_screen->height);
+               twin_pixmap_destroy(filepic);
+                              
+       }
+       twin_screen_set_background(pboot_screen, scaledpic);
+}
+
+#define PS3FB_IOCTL_SETMODE          _IOW('r',  1, int)
+#define PS3FB_IOCTL_GETMODE          _IOR('r',  2, int)
+
+static void exitfunc(void)
+{
+#ifndef _USE_X11
+       if (pboot_fbdev)
+               twin_fbdev_destroy(pboot_fbdev);
+       pboot_fbdev = NULL;
+       if (pboot_vmode_change != -1) {
+               int fd = open("/dev/fb0", O_RDWR);
+               if (fd >= 0)
+                       ioctl(fd, PS3FB_IOCTL_SETMODE,
+                             (unsigned long)&pboot_vmode_change);
+               close(fd);
+       }
+#endif
+}
+
+static void sigint(int sig)
+{
+       exitfunc();
+       syscall(__NR_exit);
+}
+
+static void usage(const char *progname)
+{
+       fprintf(stderr, "Usage: %s [-u] [-h]\n", progname);
+}
+
+int main(int argc, char **argv)
+{
+       int c;
+       int udev_trigger = 0;
+
+       for (;;) {
+               c = getopt(argc, argv, "u::h");
+               if (c == -1)
+                       break;
+
+               switch (c) {
+               case 'u':
+                       udev_trigger = 1;
+                       break;
+               case 'h':
+                       usage(argv[0]);
+                       return EXIT_SUCCESS;
+               default:
+                       fprintf(stderr, "Unknown option '%c'\n", c);
+                       usage(argv[0]);
+                       return EXIT_FAILURE;
+               }
+       }
+
+       atexit(exitfunc);
+       signal(SIGINT, sigint);
+
+#ifdef _USE_X11
+       pboot_x11 = twin_x11_create(XOpenDisplay(0), 1024, 768);
+       if (pboot_x11 == NULL) {
+               perror("failed to create x11 screen !\n");
+               return 1;
+       }
+       pboot_screen = pboot_x11->screen;
+#else
+       /* Create screen and mouse drivers */
+       pboot_fbdev = twin_fbdev_create(-1, SIGUSR1);
+       if (pboot_fbdev == NULL) {
+               perror("failed to create fbdev screen !\n");
+               return 1;
+       }
+       pboot_screen = pboot_fbdev->screen;
+       twin_linux_mouse_create(NULL, pboot_screen);
+       twin_linux_js_create(pboot_screen);
+
+       if (pboot_fbdev != NULL) {
+               char *cursor_path = artwork_pathname("cursor.gz");
+               pboot_cursor = twin_load_X_cursor(cursor_path, 2,
+                                                 &pboot_cursor_hx,
+                                                 &pboot_cursor_hy);
+               if (pboot_cursor == NULL)
+                       pboot_cursor =
+                               twin_get_default_cursor(&pboot_cursor_hx,
+                                                       &pboot_cursor_hy);
+       }
+#endif
+
+       /* Set background pixmap */
+       pboot_make_background();
+
+       /* Init more stuffs */
+       pboot_create_lpane();
+       pboot_create_rpane();
+       pboot_create_spane();
+
+       if (!pboot_start_device_discovery(udev_trigger)) {
+               LOG("Couldn't start device discovery!\n");
+               return 1;
+       }
+
+       pboot_set_lfocus(0);
+       twin_screen_set_active(pboot_screen, pboot_lpane->window->pixmap);
+       pboot_screen->event_filter = pboot_event_filter;
+
+       /* Console switch */
+#ifndef _USE_X11
+       if (pboot_fbdev)
+               twin_fbdev_activate(pboot_fbdev);
+#endif
+
+       /* Process events */
+       twin_dispatch ();
+
+       return 0;
+}