~jb55/viscal

aaca592bbe5b284355101ff5a731f3808f00535f — William Casarin 3 years ago e34b5a5
refactor progress
4 files changed, 552 insertions(+), 563 deletions(-)

A .dir-locals.el
M .gitignore
M Makefile
R calendar.c => viscal.c
A .dir-locals.el => .dir-locals.el +7 -0
@@ 0,0 1,7 @@
((c-mode . ((c-file-style . "linux")
            (indent-tabs-mode . t)
            (show-trailing-whitespace . t)
            (c-basic-offset . 8)
            (tab-width . 8)
            ))
 )

M .gitignore => .gitignore +2 -1
@@ 1,2 1,3 @@
calendar
TAGS
\ No newline at end of file
TAGS
/viscal

M Makefile => Makefile +3 -3
@@ 7,17 7,17 @@ DEPS=libical gtk+-3.0

CFLAGS=-Wall \
       -Wextra \
       -O0 \
       -O2 \
       -Wno-unused-parameter \
       -Werror=int-conversion \
			 -std=c99 \
			 -g \
			 -ggdb \
			 -lm \
       `pkg-config --cflags --libs $(DEPS)`

tags: TAGS

$(BIN): calendar.c Makefile
$(BIN): viscal.c Makefile
	$(CC) $(CFLAGS) -o $@ $<

TAGS:

R calendar.c => viscal.c +540 -559
@@ 30,7 30,7 @@ static const int EVPAD = 2;
static const int EVMARGIN = 1;
static const double ZOOM_MAX = 10;
static const double ZOOM_MIN = 1;
static const int DEF_LMARGIN = 20;
/* static const int DEF_LMARGIN = 20; */


enum event_flags {


@@ 104,59 104,10 @@ static int g_margin_time_w = 0;
static int margin_calculated = 0;

static union rgba g_text_color;
static union rgba g_timeline_color;
/* static union rgba g_timeline_color; */

static const double dashed[] = {1.0};

// calendar
static int calendar_draw (cairo_t *, struct cal*);
static struct ical* calendar_load_ical(struct cal *, char *);
static void calendar_create(struct cal *);
static void calendar_print_state(struct cal *);
static void calendar_refresh_events(struct cal*);
static void calendar_update (struct cal *);

// format
static void format_locale_time(char *, int, struct tm *);
static void format_locale_timet(char *, int, time_t);
static void format_margin_time (char *, int, int);

// draw
static void draw_background (cairo_t *, int, int);
static void draw_hours (cairo_t *, struct cal*);
static void draw_rectangle (cairo_t *, double, double);
static void draw_time_line(cairo_t *, struct cal*, time_t);

// events
static inline icaltime_span event_get_span (struct event*);
static struct event* events_hit (struct event *, int, double, double);

static icalcomponent * event_create(time_t);
static int event_hit (struct event *, double, double);
static int vevent_in_view(icalcomponent *, time_t, time_t);
static void event_draw (cairo_t *, struct cal*, struct event *);
static void event_set_summary(icalcomponent *, const char*);
static void events_for_view(struct cal *, time_t, time_t);
static void events_update_flags (struct event*, int, double, double);
static void event_update (struct event *, struct cal *cal);


// time location helpers
static double calendar_time_to_loc(struct cal *cal, time_t time);
static double time_to_location (time_t start, time_t end, time_t time);
static time_t calendar_loc_to_time(struct cal* cal, double loc);
static time_t closest_timeblock(struct cal *, int);
static time_t location_to_time(time_t start, time_t end, double loc);

// gtk events
static gboolean
  on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data);
static void on_change_view(struct cal*);
static int on_press(GtkWidget *widget, GdkEventButton *ev, gpointer user_data);
static int on_scroll(GtkWidget *widget, GdkEventScroll *ev, gpointer user_data);
static int on_motion(GtkWidget *widget, GdkEventMotion *ev, gpointer user_data);
static int on_state_change(GtkWidget *widget, GdkEvent *ev, gpointer user_data);

static void
calendar_create(struct cal *cal) {
  time_t now;


@@ 181,22 132,80 @@ calendar_create(struct cal *cal) {
  cal->zoom = 1.0;
}


static int
span_overlaps(time_t start1, time_t end1, time_t start2, time_t end2) {
	return max(0, min(end1, end2) - max(start1, start2));
}




static int
vevent_in_view(icalcomponent *vevent, time_t start, time_t end) {
	icaltime_span span = icalcomponent_get_span(vevent);
	return span_overlaps(span.start, span.end, start, end);
}

static void
events_for_view(struct cal *cal, time_t start, time_t end)
{
	int i, count = 0;
	struct event *event;
	icalcomponent *vevent;
	struct ical *calendar;
	icalcomponent *ical;

	for (i = 0; i < cal->ncalendars; ++i) {
		calendar = &cal->calendars[i];
		ical = calendar->calendar;
		for (vevent = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
		     vevent != NULL && count < MAX_EVENTS;
		     vevent = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT))
		{

			if (vevent_in_view(vevent, start, end)) {
				event = &cal->events[count++];
				/* printf("event in view %s\n", icalcomponent_get_summary(vevent)); */
				event->vevent = vevent;
				event->ical = calendar;
			}
		}
		cal->nevents = count;
	}
}


static void
on_change_view(struct cal *cal) {
  events_for_view(cal, cal->view_start, cal->view_end);
}


static void
calendar_print_state(struct cal *cal) {
	static int c = 0;
	printf("%f %d %d %s %s %d\r",
	       cal->zoom, cal->mx, cal->my,
	       (cal->flags & CAL_DRAGGING) != 0 ? "D " : "  ",
	       (cal->flags & CAL_MDOWN)    != 0 ? "M " : "  ",
	       c++
		);
	fflush(stdout);
}

static int
on_state_change(GtkWidget *widget, GdkEvent *ev, gpointer user_data) {
  struct extra_data *data = (struct extra_data*)user_data;
  struct cal *cal = data->cal;
	struct extra_data *data = (struct extra_data*)user_data;
	struct cal *cal = data->cal;

  calendar_print_state(cal);
  gtk_widget_queue_draw(widget);
	calendar_print_state(cal);
	gtk_widget_queue_draw(widget);

  return 1;
	return 1;
}


static char *
file_load(char *path) {
  FILE *f = fopen(path, "rb");


@@ 212,46 221,6 @@ file_load(char *path) {
  return string;
}

static int
span_overlaps(time_t start1, time_t end1, time_t start2, time_t end2) {
  return max(0, min(end1, end2) - max(start1, start2));
}

static int
vevent_in_view(icalcomponent *vevent, time_t start, time_t end) {
  icaltime_span span = icalcomponent_get_span(vevent);
  return span_overlaps(span.start, span.end, start, end);
}

static void
events_for_view(struct cal *cal, time_t start, time_t end)
{
  int i, count = 0;
  struct event *event;
  icalcomponent *vevent;
  struct ical *calendar;
  icalcomponent *ical;

  for (i = 0; i < cal->ncalendars; ++i) {
    calendar = &cal->calendars[i];
    ical = calendar->calendar;
    for (vevent = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
         vevent != NULL && count < MAX_EVENTS;
         vevent = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT))
    {

      if (vevent_in_view(vevent, start, end)) {
        event = &cal->events[count++];
        /* printf("event in view %s\n", icalcomponent_get_summary(vevent)); */
        event->vevent = vevent;
        event->ical = calendar;
      }
    }
    cal->nevents = count;
  }
}


static struct ical *
calendar_load_ical(struct cal *cal, char *path) {
  // TODO: don't load duplicate calendars


@@ 275,21 244,21 @@ calendar_load_ical(struct cal *cal, char *path) {
}


static void
event_set_start(struct event *ev, time_t time, const icaltimezone *zone) {
  if (zone == NULL)
    zone = g_timezone;
  icaltimetype ictime = icaltime_from_timet_with_zone(time, 1, zone);
  icalcomponent_set_dtstart(ev->vevent, ictime);
}
/* static void */
/* event_set_start(struct event *ev, time_t time, const icaltimezone *zone) { */
/*   if (zone == NULL) */
/*     zone = g_timezone; */
/*   icaltimetype ictime = icaltime_from_timet_with_zone(time, 1, zone); */
/*   icalcomponent_set_dtstart(ev->vevent, ictime); */
/* } */

static void
event_set_end(struct event *ev, time_t time, const icaltimezone *zone) {
  if (zone == NULL)
    zone = g_timezone;
  icaltimetype ictime = icaltime_from_timet_with_zone(time, 1, zone);
  icalcomponent_set_dtend(ev->vevent, ictime);
}
/* static void */
/* event_set_end(struct event *ev, time_t time, const icaltimezone *zone) { */
/*   if (zone == NULL) */
/*     zone = g_timezone; */
/*   icaltimetype ictime = icaltime_from_timet_with_zone(time, 1, zone); */
/*   icalcomponent_set_dtend(ev->vevent, ictime); */
/* } */





@@ 318,13 287,29 @@ calendar_drop(struct cal *cal, double mx, double my) {
  }
}


static time_t
location_to_time(time_t start, time_t end, double loc) {
	return (time_t)((double)start) + (loc * (end - start));
}



static time_t
calendar_loc_to_time(struct cal *cal, double y) {
	// XXX: this is wrong wrt. zoom
	return location_to_time(cal->view_start, cal->view_end,
				y/((double)cal->height * cal->zoom));
}

static void
event_click(struct cal *cal, struct event *event, int mx, int my) {
  printf("clicked %s\n", icalcomponent_get_summary(event->vevent));
	printf("clicked %s\n", icalcomponent_get_summary(event->vevent));

  calendar_loc_to_time(cal, my);
	calendar_loc_to_time(cal, my);
}


static icalcomponent *
event_create(time_t time) {
  icalcomponent *vevent;


@@ 346,11 331,62 @@ calendar_refresh_events(struct cal *cal) {
  gtk_widget_queue_draw(cal->widget);
}


static time_t
closest_timeblock(struct cal *cal, int y) {
	time_t st;
	struct tm lt;
	st = calendar_loc_to_time(cal, y);
	lt = *localtime(&st);
	lt.tm_min = round(lt.tm_min / cal->minute_round) * cal->minute_round;
	lt.tm_sec = 0; // removes jitter
	return mktime(&lt);
}

// TODO: this should handle zh_CN and others as well
void time_remove_seconds(char *time, int n) {
	int len = strlen(time);
	int count = 0;
	char *ws;
	for (int i = 0; i < len; ++i) {
		if (count == n) {
			ws = &time[i];
			while (*ws != '\0' &&
			       (*ws == ':' ||
				(*ws >= '0' && *ws <= '9'))) ws++;
			len = strlen(ws);
			memcpy(&time[i-1], ws, len);
			time[i-1+len] = '\0';
			return;
		}
		// FIXME: instead of (==':'), we want (!= 0..9), in a unicode-enumerated way
		count += time[i] == ':' ? 1 : 0;
	}
}



static void
format_locale_time(char *buffer, int bsize, struct tm *tm) {
	strftime(buffer, bsize, "%X", tm);
	time_remove_seconds(buffer, 2);
}



static void
format_locale_timet(char *buffer, int bsize, time_t time) {
	struct tm lt;
	lt = *localtime(&time);
	format_locale_time(buffer, bsize, &lt);
}


static void
calendar_view_clicked(struct cal *cal, int mx, int my) {
  icalcomponent *vevent;
  time_t closest;
  int y;
  /* int y; */
  char buf[32];

  closest = closest_timeblock(cal, my);


@@ 369,9 405,29 @@ calendar_view_clicked(struct cal *cal, int mx, int my) {
  icalcomponent_add_component(calendar_def_cal(cal), vevent);
  calendar_refresh_events(cal);

  y = calendar_time_to_loc(cal, closest) * cal->height;
  /* y = calendar_time_to_loc(cal, closest) * cal->height; */
}

static int
event_hit (struct event *ev, double mx, double my) {
	return
		mx >= ev->x
		&& mx <= (ev->x + ev->width)
		&& my >= ev->y
		&& my <= (ev->y + ev->height);
}


static struct event*
events_hit (struct event *events, int nevents, double mx, double my) {
	for (int i = 0; i < nevents; ++i) {
		if (event_hit(&events[i], mx, my))
			return &events[i];
	}
	return NULL;
}


static int
on_press(GtkWidget *widget, GdkEventButton *ev, gpointer user_data) {
  struct extra_data *data = (struct extra_data*)user_data;


@@ 432,18 488,6 @@ event_any_flags(struct event *events, int nevents, int flag) {
  return NULL;
}

static void
calendar_print_state(struct cal *cal) {
  static int c = 0;
  printf("%f %d %d %s %s %d\r",
         cal->zoom, cal->mx, cal->my,
         (cal->flags & CAL_DRAGGING) != 0 ? "D " : "  ",
         (cal->flags & CAL_MDOWN)    != 0 ? "M " : "  ",
         c++
         );
  fflush(stdout);
}

static int
on_scroll(GtkWidget *widget, GdkEventScroll *ev, gpointer user_data) {
  // TODO: GtkGestureZoom


@@ 467,540 511,477 @@ on_scroll(GtkWidget *widget, GdkEventScroll *ev, gpointer user_data) {
  return 0;
}

static int
on_motion(GtkWidget *widget, GdkEventMotion *ev, gpointer user_data) {
  static struct event* prev_hit = NULL;
static void
update_event_flags (struct event *ev, double mx, double my) {
	int hit = event_hit(ev, mx, my);
	if (hit) ev->flags |=  EV_HIGHLIGHTED;
	else     ev->flags &= ~EV_HIGHLIGHTED;
}

  struct event *hit = NULL;
  int state_changed = 0;
  int dragging_event = 0;
  double mx = ev->x;
  double my = ev->y;

  struct extra_data *data = (struct extra_data*)user_data;
  struct cal *cal = data->cal;
  GdkWindow *gdkwin = gtk_widget_get_window(widget);
static void
events_update_flags (struct event *events, int nevents, double mx, double my) {
	for (int i = 0; i < nevents; ++i) {
		struct event *ev = &events[i];
		update_event_flags (ev, mx, my);
	}
}

  cal->mx = mx - cal->x;
  cal->my = my - cal->y;

  double px = ev->x;
  double py = ev->y;

  // drag detection
  if ((cal->flags & CAL_MDOWN) != 0) {
    if ((cal->flags & CAL_DRAGGING) == 0)
      cal->flags |= CAL_DRAGGING;
  }
static int
on_motion(GtkWidget *widget, GdkEventMotion *ev, gpointer user_data) {
	static struct event* prev_hit = NULL;

  // dragging logic
  if ((cal->flags & CAL_DRAGGING) != 0) {
    if (cal->target) {
      dragging_event = 1;
      cal->target->dragx = px - cal->target->x;
      cal->target->dragy = py - cal->target->y - cal->y;
    }
  }
	struct event *hit = NULL;
	int state_changed = 0;
	int dragging_event = 0;
	double mx = ev->x;
	double my = ev->y;

  events_update_flags (cal->events, cal->nevents, mx, my);
  hit = event_any_flags(cal->events, cal->nevents, EV_HIGHLIGHTED);
	struct extra_data *data = (struct extra_data*)user_data;
	struct cal *cal = data->cal;
	GdkWindow *gdkwin = gtk_widget_get_window(widget);

  gdk_window_set_cursor(gdkwin, hit ? cursor_pointer : cursor_default);
	cal->mx = mx - cal->x;
	cal->my = my - cal->y;

  state_changed = dragging_event || hit != prev_hit;
	double px = ev->x;
	double py = ev->y;

  fflush(stdout);
  prev_hit = hit;
	// drag detection
	if ((cal->flags & CAL_MDOWN) != 0)
	if ((cal->flags & CAL_DRAGGING) == 0)
		cal->flags |= CAL_DRAGGING;

  if (state_changed)
    on_state_change(widget, (GdkEvent*)ev, user_data);
		// dragging logic
	if ((cal->flags & CAL_DRAGGING) != 0) {
		if (cal->target) {
			dragging_event = 1;
			cal->target->dragx = px - cal->target->x;
			cal->target->dragy = py - cal->target->y - cal->y;
		}
	}

  return 1;
}
	events_update_flags (cal->events, cal->nevents, mx, my);
	hit = event_any_flags(cal->events, cal->nevents, EV_HIGHLIGHTED);

static struct event*
events_hit (struct event *events, int nevents, double mx, double my) {
  for (int i = 0; i < nevents; ++i) {
    if (event_hit(&events[i], mx, my))
      return &events[i];
  }
  return NULL;
}

static int
event_hit (struct event *ev, double mx, double my) {
  return
    mx >= ev->x
    && mx <= (ev->x + ev->width)
    && my >= ev->y
    && my <= (ev->y + ev->height);
}

static void
update_event_flags (struct event *ev, double mx, double my) {
  int hit = event_hit(ev, mx, my);
  if (hit) ev->flags |=  EV_HIGHLIGHTED;
  else     ev->flags &= ~EV_HIGHLIGHTED;
}

static void
events_update_flags (struct event *events, int nevents, double mx, double my) {
  for (int i = 0; i < nevents; ++i) {
    struct event *ev = &events[i];
    update_event_flags (ev, mx, my);
  }
}
	gdk_window_set_cursor(gdkwin, hit ? cursor_pointer : cursor_default);

	state_changed = dragging_event || hit != prev_hit;

static gboolean
on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
  int width, height;
  struct extra_data *data = (struct extra_data*) user_data;
  struct cal *cal = data->cal;
	fflush(stdout);
	prev_hit = hit;

  if (!margin_calculated) {
    char buffer[32];
    cairo_text_extents_t exts;
	if (state_changed)
	on_state_change(widget, (GdkEvent*)ev, user_data);

    format_margin_time(buffer, 32, 23);
    cairo_text_extents(cr, buffer, &exts);
    g_margin_time_w = exts.width;
    g_lmargin = g_margin_time_w + EVPAD*2;
	return 1;
}

    margin_calculated = 1;
  }

  cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
static void
format_margin_time(char *buffer, int bsize, int hour) {
	struct tm tm = { .tm_min = 0, .tm_hour = hour };
	strftime(buffer, bsize, "%X", &tm);
	time_remove_seconds(buffer, 1);
}

  gtk_window_get_size(data->win, &width, &height);

  cal->y = cal->gutter_height;
static double
time_to_location (time_t start, time_t end, time_t time) {
	return ((double)(time - start) / ((double)(end - start)));
}

  cal->width = width - cal->x;
  cal->height = height - cal->y;

  calendar_update(cal);
  calendar_draw(cr, cal);

  return FALSE;
static double
calendar_time_to_loc(struct cal *cal, time_t time) {
	// ZOOM
	return time_to_location(cal->view_start, cal->view_end, time) * cal->zoom;
}


static void draw_background (cairo_t *cr, int width, int height) {
  cairo_set_source_rgb (cr, 0.3, 0.3, 0.3);
  draw_rectangle (cr, width, height);
  cairo_fill (cr);
}

static inline void
draw_line (cairo_t *cr, double x, double y, double w) {
  cairo_move_to(cr, x, y + 0.5);
  cairo_rel_line_to(cr, w, 0);
static void
event_update (struct event *ev, struct cal *cal)
{
	icaltimetype evtime = icalcomponent_get_dtstart(ev->vevent);
	icaltime_span span = icalcomponent_get_span(ev->vevent);
	int isdate = evtime.is_date;
	double sx, sy, y, eheight, height, width;

	height = cal->height;
	width = cal->width;

	sx = cal->x;
	sy = cal->y;

	// height is fixed in top gutter for date events
	if (isdate) {
		// TODO: (DATEEV) gutter positioning
		eheight = 20.0;
		y = EVPAD;
	}
	else {
		double sloc = calendar_time_to_loc(cal, span.start);
		double eloc = calendar_time_to_loc(cal, span.end);

		double dloc = eloc - sloc;
		eheight = dloc * height;
		y = (sloc * height) + sy;
	}

	ev->width = width;
	ev->height = eheight;
	ev->x = sx;
	ev->y = y;
}

static void
draw_time_line(cairo_t *cr, struct cal *cal, time_t time) {
  double loc = calendar_time_to_loc(cal, time);
  double y = (loc * cal->height) + cal->y;
  int w = cal->width;
calendar_update (struct cal *cal) {
	int i, width, height;
	width = cal->width;
	height = cal->height;

  cairo_set_line_width(cr, 1.0);
	width  -= cal->x;
	height -= cal->y * 2;

  cairo_set_source_rgb (cr, 1.0, 0, 0);
  draw_line(cr, cal->x, y - 1, w);
  cairo_stroke(cr);
	if (cal->refresh_events) {
		on_change_view(cal);
		cal->refresh_events = 0;
	}

	for (i = 0; i < cal->nevents; ++i) {
		struct event *ev = &cal->events[i];
		event_update(ev, cal);
	}
}

  /* cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); */
  /* draw_line(cr, cal->x, y, w); */
  /* cairo_stroke(cr); */

  /* cairo_set_source_rgb (cr, 0, 0, 0); */
  /* draw_line(cr, cal->x, y + 1, w); */
  /* cairo_stroke(cr); */
} 


static void draw_rectangle (cairo_t *cr, double x, double y) {
  cairo_rel_line_to (cr, x, 0);
  cairo_rel_line_to (cr, 0, y);
  cairo_rel_line_to (cr, -x, 0);
  cairo_close_path (cr);
	cairo_rel_line_to (cr, x, 0);
	cairo_rel_line_to (cr, 0, y);
	cairo_rel_line_to (cr, -x, 0);
	cairo_close_path (cr);
}


// TODO: this should handle zh_CN and others as well
void time_remove_seconds(char *time, int n) {
  int len = strlen(time);
  int count = 0;
  char *ws;
  for (int i = 0; i < len; ++i) {
    if (count == n) {
      ws = &time[i];
      while (*ws != '\0' && (*ws == ':' || (*ws >= '0' && *ws <= '9'))) ws++;
      len = strlen(ws);
      memcpy(&time[i-1], ws, len);
      time[i-1+len] = '\0';
      return;
    }
    // FIXME: instead of (==':'), we want (!= 0..9), in a unicode-enumerated way
    count += time[i] == ':' ? 1 : 0;
  }
static void draw_background (cairo_t *cr, int width, int height) {
	cairo_set_source_rgb (cr, 0.3, 0.3, 0.3);
	draw_rectangle (cr, width, height);
	cairo_fill (cr);
}


static void
format_margin_time(char *buffer, int bsize, int hour) {
  struct tm tm = { .tm_min = 0, .tm_hour = hour };
  strftime(buffer, bsize, "%X", &tm);
  time_remove_seconds(buffer, 1);
draw_hours (cairo_t *cr, struct cal* cal)
{
	double height = cal->height;
	double width  = cal->width;
	double zoom   = cal->zoom;

	double section_height = (((double)height) / 48.0) * zoom;
	char buffer[32] = {0};
	const double col = 0.4;
	cairo_set_source_rgb (cr, col, col, col);
	cairo_set_line_width (cr, 1);

	// TODO: dynamic section subdivide on zoom?
	for (int section = 0; section < 48; section++) {
		int minutes = section * 30;
		int onhour = ((minutes / 30) % 2) == 0;
		if (section_height < 14 && !onhour)
			continue;
		double y = cal->y + ((double)section) * section_height;
		cairo_move_to (cr, cal->x, y);
		cairo_rel_line_to (cr, width, 0);

		if (section % 2 == 0)
			cairo_set_dash (cr, NULL, 0, 0);
		else
			cairo_set_dash (cr, dashed, 1, 0);

		cairo_stroke(cr);
		cairo_set_dash (cr, NULL, 0, 0);

		if (onhour) {
			format_margin_time(buffer, 32, minutes / 60);
			// TODO: text extents for proper time placement?
			cairo_move_to(cr, g_lmargin - (g_margin_time_w + EVPAD),
				      y+TXTPAD);
			cairo_set_source_rgb (cr,
						g_text_color.r,
						g_text_color.g,
						g_text_color.b);
			cairo_show_text(cr, buffer);
			cairo_set_source_rgb (cr, col, col, col);
		}
	}
}


static void
format_locale_timet(char *buffer, int bsize, time_t time) {
  struct tm lt;
  lt = *localtime(&time);
  format_locale_time(buffer, bsize, &lt);
event_draw (cairo_t *cr, struct cal *cal, struct event *ev) {
	// double height = Math.fmin(, MIN_EVENT_HEIGHT);
	// stdout.printf("sloc %f eloc %f dloc %f eheight %f\n",
	// 			  sloc, eloc, dloc, eheight);
	static char bsmall[32] = {0};
	static char bsmall2[32] = {0};
	static char buffer[1024] = {0};

	union rgba c = ev->ical->color;
	int is_dragging = cal->target == ev && (cal->flags & CAL_DRAGGING);
	double evheight = max(1.0, ev->height - EVMARGIN);
	/* double evwidth = ev->width; */
	icaltimetype dtstart = icalcomponent_get_dtstart(ev->vevent);
	icaltimetype dtend = icalcomponent_get_dtend(ev->vevent);
	int isdate = dtstart.is_date;
	double x = ev->x;
	// TODO: date-event stacking
	double y = ev->y;
	time_t st = icaltime_as_timet(dtstart);
	time_t et = icaltime_as_timet(dtend);
	time_t len = et - st;
	cairo_text_extents_t exts;
	const char * const summary = icalcomponent_get_summary(ev->vevent);

	if (is_dragging || ev->flags & EV_HIGHLIGHTED) {
		c.a *= 0.95;
	}

	// grid logic
	if (is_dragging) {
		/* x += ev->dragx; */
		y += ev->dragy;
		st = closest_timeblock(cal, y);
		y = cal->y + calendar_time_to_loc(cal, st) * cal->height;
		cal->target->drag_time = st;
	}

	/* y -= EVMARGIN; */

	cairo_move_to(cr, x, y);
	cairo_set_source_rgba(cr, c.r, c.g, c.b, c.a);
	draw_rectangle(cr, ev->width, evheight);
	cairo_fill(cr);
	// TODO: event text color
	static const double txtc = 0.2;
	cairo_set_source_rgb(cr, txtc, txtc, txtc);
	if (isdate) {
		sprintf(buffer, "%s", summary);
		cairo_text_extents(cr, buffer, &exts);
		cairo_move_to(cr, x + EVPAD, y + (evheight / 2.0)
						+ ((double)exts.height / 2.0));
		cairo_show_text(cr, buffer);
	}
	/* else if (len > 30*60) { */
	/*   format_locale_timet(bsmall, 32, st); */
	/*   format_locale_timet(bsmall2, 32, et); */
	/*   sprintf(buffer, "%s — %s", bsmall, bsmall2); */
	/*   cairo_show_text(cr, buffer); */
	/*   cairo_move_to(cr, x + EVPAD, y + EVPAD + TXTPAD * 2); */
	/*   cairo_show_text(cr, summary); */
	/* } */
	else {
		format_locale_timet(bsmall, 32, st);
		format_locale_timet(bsmall2, 32, et);
		// TODO: configurable event format
		sprintf(buffer, "%d  %s", (int)len / 60, summary);
		cairo_text_extents(cr, buffer, &exts);
		double ey = evheight < exts.height
			? y + TXTPAD - EVPAD
			: y + TXTPAD + EVPAD;
		cairo_move_to(cr, x + EVPAD, ey);
		cairo_show_text(cr, buffer);
	}
}

static void
format_locale_time(char *buffer, int bsize, struct tm *tm) {
  strftime(buffer, bsize, "%X", tm);
  time_remove_seconds(buffer, 2);

static inline void
draw_line (cairo_t *cr, double x, double y, double w) {
	cairo_move_to(cr, x, y + 0.5);
	cairo_rel_line_to(cr, w, 0);
}



static void
draw_hours (cairo_t *cr, struct cal* cal)
{
  double height = cal->height;
  double width  = cal->width;
  double zoom   = cal->zoom;

  double section_height = (((double)height) / 48.0) * zoom;
  char buffer[32] = {0};
  const double col = 0.4;
  cairo_set_source_rgb (cr, col, col, col);
  cairo_set_line_width (cr, 1);

  // TODO: dynamic section subdivide on zoom?
  for (int section = 0; section < 48; section++) {
    int minutes = section * 30;
    int onhour = ((minutes / 30) % 2) == 0;
    if (section_height < 14 && !onhour) continue;
    double y = cal->y + ((double)section) * section_height;
    cairo_move_to (cr, cal->x, y);
    cairo_rel_line_to (cr, width, 0);

    if (section % 2 == 0)
      cairo_set_dash (cr, NULL, 0, 0);
    else
      cairo_set_dash (cr, dashed, 1, 0);

    cairo_stroke(cr);
    cairo_set_dash (cr, NULL, 0, 0);

    if (onhour) {
      format_margin_time(buffer, 32, minutes / 60);
      // TODO: text extents for proper time placement?
      cairo_move_to(cr, g_lmargin - (g_margin_time_w + EVPAD), y+TXTPAD);
      cairo_set_source_rgb (cr,
                            g_text_color.r,
                            g_text_color.g,
                            g_text_color.b);
      cairo_show_text(cr, buffer);
      cairo_set_source_rgb (cr, col, col, col);
    }
  }
}
draw_time_line(cairo_t *cr, struct cal *cal, time_t time) {
	double loc = calendar_time_to_loc(cal, time);
	double y = (loc * cal->height) + cal->y;
	int w = cal->width;

static time_t
calendar_loc_to_time(struct cal *cal, double y) {
  // XXX: this is wrong wrt. zoom
  return location_to_time(cal->view_start, cal->view_end,
                          y/((double)cal->height * cal->zoom));
}
	cairo_set_line_width(cr, 1.0);

static double
calendar_time_to_loc(struct cal *cal, time_t time) {
  // ZOOM
  return time_to_location (cal->view_start, cal->view_end, time) * cal->zoom;
}
	cairo_set_source_rgb (cr, 1.0, 0, 0);
	draw_line(cr, cal->x, y - 1, w);
	cairo_stroke(cr);

	/* cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); */
	/* draw_line(cr, cal->x, y, w); */
	/* cairo_stroke(cr); */

static time_t
location_to_time(time_t start, time_t end, double loc) {
  return (time_t)((double)start) + (loc * (end - start));
}
	/* cairo_set_source_rgb (cr, 0, 0, 0); */
	/* draw_line(cr, cal->x, y + 1, w); */
	/* cairo_stroke(cr); */
} 

static double
time_to_location (time_t start, time_t end, time_t time) {
  return ((double)(time - start) / ((double)(end - start)));
}

static void
event_update (struct event *ev, struct cal *cal)
{
  icaltimetype evtime = icalcomponent_get_dtstart(ev->vevent);
  icaltime_span span = icalcomponent_get_span(ev->vevent);
  int isdate = evtime.is_date;
  double sx, sy, y, eheight, height, width;

  height = cal->height;
  width = cal->width;

  sx = cal->x;
  sy = cal->y;

  // height is fixed in top gutter for date events
  if (isdate) {
    // TODO: (DATEEV) gutter positioning
    eheight = 20.0;
    y = EVPAD;
  }
  else {
    double sloc = calendar_time_to_loc(cal, span.start);
    double eloc = calendar_time_to_loc(cal, span.end);
static int
calendar_draw (cairo_t *cr, struct cal *cal) {
	int i, width, height;
	time_t now;
	width = cal->width;
	height = cal->height;

    double dloc = eloc - sloc;
    eheight = dloc * height;
    y = (sloc * height) + sy;
  }
	cairo_move_to(cr, cal->x, cal->y);
	draw_background(cr, width, height);
	draw_hours(cr, cal);

  ev->width = width;
  ev->height = eheight;
  ev->x = sx;
  ev->y = y;
}
	// draw calendar events
	for (i = 0; i < cal->nevents; ++i) {
		struct event *ev = &cal->events[i];
		event_draw(cr, cal, ev);
	}

static time_t
closest_timeblock(struct cal *cal, int y) {
  time_t st;
  struct tm lt;
  st = calendar_loc_to_time(cal, y);
  lt = *localtime(&st);
  lt.tm_min = round(lt.tm_min / cal->minute_round) * cal->minute_round;
  lt.tm_sec = 0; // removes jitter
  return mktime(&lt);
	draw_time_line(cr, cal, time(&now));

	return 1;
}

static void
event_draw (cairo_t *cr, struct cal *cal, struct event *ev) {
  // double height = Math.fmin(, MIN_EVENT_HEIGHT);
  // stdout.printf("sloc %f eloc %f dloc %f eheight %f\n",
  // 			  sloc, eloc, dloc, eheight);
  static char bsmall[32] = {0};
  static char bsmall2[32] = {0};
  static char buffer[1024] = {0};

  union rgba c = ev->ical->color;
  int is_dragging = cal->target == ev && (cal->flags & CAL_DRAGGING);
  double evheight = max(1.0, ev->height - EVMARGIN);
  double evwidth = ev->width;
  icaltimetype dtstart = icalcomponent_get_dtstart(ev->vevent);
  icaltimetype dtend = icalcomponent_get_dtend(ev->vevent);
  int isdate = dtstart.is_date;
  double x = ev->x;
  // TODO: date-event stacking
  double y = ev->y;
  time_t st = icaltime_as_timet(dtstart);
  time_t et = icaltime_as_timet(dtend);
  time_t len = et - st;
  cairo_text_extents_t exts;
  const char * const summary = icalcomponent_get_summary(ev->vevent);

  if (is_dragging || ev->flags & EV_HIGHLIGHTED) {
    c.a *= 0.95;
  }
static gboolean
on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
	int width, height;
	struct extra_data *data = (struct extra_data*) user_data;
	struct cal *cal = data->cal;

  // grid logic
  if (is_dragging) {
    /* x += ev->dragx; */
    y += ev->dragy;
    st = closest_timeblock(cal, y);
    y = cal->y + calendar_time_to_loc(cal, st) * cal->height;
    cal->target->drag_time = st;
  }
	if (!margin_calculated) {
		char buffer[32];
		cairo_text_extents_t exts;

  /* y -= EVMARGIN; */

  cairo_move_to(cr, x, y);
  cairo_set_source_rgba(cr, c.r, c.g, c.b, c.a);
  draw_rectangle(cr, ev->width, evheight);
  cairo_fill(cr);
  // TODO: event text color
  static const double txtc = 0.2;
  cairo_set_source_rgb(cr, txtc, txtc, txtc);
  if (isdate) {
    sprintf(buffer, "%s", summary);
    cairo_text_extents(cr, buffer, &exts);
    cairo_move_to(cr, x + EVPAD, y + (evheight / 2.0)
                                   + ((double)exts.height / 2.0));
    cairo_show_text(cr, buffer);
  }
  /* else if (len > 30*60) { */
  /*   format_locale_timet(bsmall, 32, st); */
  /*   format_locale_timet(bsmall2, 32, et); */
  /*   sprintf(buffer, "%s — %s", bsmall, bsmall2); */
  /*   cairo_show_text(cr, buffer); */
  /*   cairo_move_to(cr, x + EVPAD, y + EVPAD + TXTPAD * 2); */
  /*   cairo_show_text(cr, summary); */
  /* } */
  else {
    format_locale_timet(bsmall, 32, st);
    format_locale_timet(bsmall2, 32, et);
    // TODO: configurable event format
    sprintf(buffer, "%d  %s", len / 60, summary);
    cairo_text_extents(cr, buffer, &exts);
    double ey = evheight < exts.height
              ? y + TXTPAD - EVPAD
              : y + TXTPAD + EVPAD;
    cairo_move_to(cr, x + EVPAD, ey);
    cairo_show_text(cr, buffer);
  }
}
		format_margin_time(buffer, 32, 23);
		cairo_text_extents(cr, buffer, &exts);
		g_margin_time_w = exts.width;
		g_lmargin = g_margin_time_w + EVPAD*2;

static void
calendar_update (struct cal *cal) {
  int i, width, height;
  width = cal->width;
  height = cal->height;
		margin_calculated = 1;
	}

  width  -= cal->x;
  height -= cal->y * 2;
	cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);

  if (cal->refresh_events) {
    on_change_view(cal);
    cal->refresh_events = 0;
  }
	gtk_window_get_size(data->win, &width, &height);

  for (i = 0; i < cal->nevents; ++i) {
    struct event *ev = &cal->events[i];
    event_update(ev, cal);
  }
}
	cal->y = cal->gutter_height;

static int
calendar_draw (cairo_t *cr, struct cal *cal) {
  int i, width, height;
  time_t now;
  width = cal->width;
  height = cal->height;
	cal->width = width - cal->x;
	cal->height = height - cal->y;

  cairo_move_to(cr, cal->x, cal->y);
  draw_background(cr, width, height);
  draw_hours(cr, cal);
	calendar_update(cal);
	calendar_draw(cr, cal);

  // draw calendar events
  for (i = 0; i < cal->nevents; ++i) {
    struct event *ev = &cal->events[i];
    event_draw(cr, cal, ev);
  }
	return FALSE;
}

  draw_time_line(cr, cal, time(&now));

  return 1;
void usage() {
	printf("usage: viscal <calendar.ics ...>\n");
	exit(1);
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;
  GdkDisplay *display;
  GdkColor color;
  char buffer[32];
  double text_col = 0.6;
  struct ical *ical;
  union rgba defcol;
	GtkWidget *window;
	GtkWidget *darea;
	GdkDisplay *display;
	GdkColor color;
	char buffer[32];
	double text_col = 0.6;
	struct ical *ical;
	union rgba defcol;

  defcol.r = 106.0 / 255.0;
  defcol.g = 219.0 / 255.0;
  defcol.b = 219.0 / 255.0;
  defcol.a = 1.0;
	defcol.r = 106.0 / 255.0;
	defcol.g = 219.0 / 255.0;
	defcol.b = 219.0 / 255.0;
	defcol.a = 1.0;

  struct cal cal;
	struct cal cal;

  calendar_create(&cal);
	calendar_create(&cal);

  ical = calendar_load_ical(&cal, "/home/jb55/var/ical2org/personal.ical");
  if (ical)
    ical->color = defcol;
	if (argc < 2)
		usage();

  ical = calendar_load_ical(&cal, "/home/jb55/var/ical2org/monstercat.ical");
  if (ical) {
    ical->color = defcol;
    ical->color.r *= 0.5;
  }
	printf("loading calendar %s\n", argv[1]);
	ical = calendar_load_ical(&cal, argv[1]);
	ical->color = defcol;

  ical = calendar_load_ical(&cal, "/home/jb55/var/ical2org/billnessa.ical");
  if (ical) {
    ical->color = defcol;
    ical->color.b *= 0.5;
  }
	on_change_view(&cal);

  on_change_view(&cal);
	// TODO: get system timezone
	g_timezone = icaltimezone_get_builtin_timezone("America/Vancouver");

  // TODO: get system timezone
  g_timezone = icaltimezone_get_builtin_timezone("America/Vancouver");
	g_text_color.r = text_col;
	g_text_color.g = text_col;
	g_text_color.b = text_col;

  g_text_color.r = text_col;
  g_text_color.g = text_col;
  g_text_color.b = text_col;
	color.red = BGCOLOR * 0xffff * 0.6;
	color.green = BGCOLOR * 0xffff * 0.6;
	color.blue = BGCOLOR * 0xffff * 0.6;

  color.red = BGCOLOR * 0xffff * 0.6;
  color.green = BGCOLOR * 0xffff * 0.6;
  color.blue = BGCOLOR * 0xffff * 0.6;
	/* setlocale(LC_TIME, ""); */

  /* setlocale(LC_TIME, ""); */
	// calc margin
	format_margin_time(buffer, 32, 12);

  // calc margin
  format_margin_time(buffer, 32, 12);
	gtk_init(&argc, &argv);

  gtk_init(&argc, &argv);
	window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	struct extra_data extra_data = {
		.win = GTK_WINDOW(window),
		.cal = &cal
	};

  struct extra_data extra_data = {
    .win = GTK_WINDOW(window),
    .cal = &cal
  };

  display = gdk_display_get_default();
  darea = gtk_drawing_area_new();
  cal.widget = darea;
  gtk_container_add(GTK_CONTAINER(window), darea);
	display = gdk_display_get_default();
	darea = gtk_drawing_area_new();
	cal.widget = darea;
	gtk_container_add(GTK_CONTAINER(window), darea);

  cursor_pointer = gdk_cursor_new_from_name (display, "pointer");
  cursor_default = gdk_cursor_new_from_name (display, "default");
	cursor_pointer = gdk_cursor_new_from_name (display, "pointer");
	cursor_default = gdk_cursor_new_from_name (display, "default");

  g_signal_connect(G_OBJECT(darea), "button-press-event",
                   G_CALLBACK(on_press), (gpointer)&extra_data);
  g_signal_connect(G_OBJECT(darea), "button-release-event",
                   G_CALLBACK(on_press), (gpointer)&extra_data);
  g_signal_connect(G_OBJECT(darea), "motion-notify-event",
                   G_CALLBACK(on_motion), (gpointer)&extra_data);
  g_signal_connect(G_OBJECT(darea), "scroll-event",
                   G_CALLBACK(on_scroll), (gpointer)&extra_data);
	g_signal_connect(G_OBJECT(darea), "button-press-event",
			G_CALLBACK(on_press), (gpointer)&extra_data);
	g_signal_connect(G_OBJECT(darea), "button-release-event",
			G_CALLBACK(on_press), (gpointer)&extra_data);
	g_signal_connect(G_OBJECT(darea), "motion-notify-event",
			G_CALLBACK(on_motion), (gpointer)&extra_data);
	g_signal_connect(G_OBJECT(darea), "scroll-event",
			G_CALLBACK(on_scroll), (gpointer)&extra_data);

  g_signal_connect(G_OBJECT(darea), "draw",
                   G_CALLBACK(on_draw_event), (gpointer)&extra_data);
	g_signal_connect(G_OBJECT(darea), "draw",
			G_CALLBACK(on_draw_event), (gpointer)&extra_data);

  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);
	g_signal_connect(window, "destroy",
	G_CALLBACK(gtk_main_quit), NULL);

  gtk_widget_set_events(darea, GDK_BUTTON_PRESS_MASK
                             | GDK_BUTTON_RELEASE_MASK
                             | GDK_SCROLL_MASK
                             | GDK_SMOOTH_SCROLL_MASK
                             | GDK_POINTER_MOTION_MASK);
  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 800);
  gtk_window_set_title(GTK_WINDOW(window), "viscal");
	gtk_widget_set_events(darea, GDK_BUTTON_PRESS_MASK
				| GDK_BUTTON_RELEASE_MASK
				| GDK_SCROLL_MASK
				| GDK_SMOOTH_SCROLL_MASK
				| GDK_POINTER_MOTION_MASK);
	gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
	gtk_window_set_default_size(GTK_WINDOW(window), 400, 800);
	gtk_window_set_title(GTK_WINDOW(window), "viscal");

  gtk_widget_modify_bg(window, GTK_STATE_NORMAL, &color);
  gtk_widget_show_all(window);
	gtk_widget_modify_bg(window, GTK_STATE_NORMAL, &color);
	gtk_widget_show_all(window);

  gtk_main();
	gtk_main();

  return 0;
	return 0;
}