viscal

cairo/gtk vi-like timeblocking calendar
git clone git://jb55.com/viscal
Log | Files | Refs | README | LICENSE

commit aaca592bbe5b284355101ff5a731f3808f00535f
parent e34b5a50e96269fd0aac1cd17995b5a1776bb40c
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 15 Aug 2018 18:56:59 -0700

refactor progress

Diffstat:
A.dir-locals.el | 7+++++++
M.gitignore | 4++--
MMakefile | 6+++---
Dcalendar.c | 1006-------------------------------------------------------------------------------
Aviscal.c | 987+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 999 insertions(+), 1011 deletions(-)

diff --git a/.dir-locals.el b/.dir-locals.el @@ -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) + )) + ) diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ calendar -TAGS- \ No newline at end of file +TAGS +/viscal diff --git a/Makefile b/Makefile @@ -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: diff --git a/calendar.c b/calendar.c @@ -1,1006 +0,0 @@ - -#include <cairo/cairo.h> -#include <gtk/gtk.h> -#include <libical/ical.h> -#include <assert.h> -#include <time.h> -#include <string.h> -#include <stdlib.h> -#include <math.h> -#include <locale.h> - -#define length(array) (sizeof((array))/sizeof((array)[0])) - -#define max(a,b) \ - ({ __typeof__ (a) _a = (a); \ - __typeof__ (b) _b = (b); \ - _a > _b ? _a : _b; }) - -#define min(a,b) \ - ({ __typeof__ (a) _a = (a); \ - __typeof__ (b) _b = (b); \ - _a < _b ? _a : _b; }) - -#define MAX_EVENTS 1024 - -static const double BGCOLOR = 0.35; -static const int DAY_SECONDS = 86400; -static const int TXTPAD = 11; -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; - - -enum event_flags { - EV_SELECTED = 1 << 0 - , EV_HIGHLIGHTED = 1 << 1 - , EV_DRAGGING = 1 << 2 -}; - -enum cal_flags { - CAL_MDOWN = 1 << 0 - , CAL_DRAGGING = 1 << 1 -}; - -union rgba { - double rgba[4]; - struct { - double r, g, b, a; - }; -}; - -struct ical { - icalcomponent * calendar; - union rgba color; -}; - -struct event { - icalcomponent *vevent; - struct ical *ical; - - enum event_flags flags; - // set on draw - double width, height; - double x, y; - double dragx, dragy; - time_t drag_time; -}; - -struct cal { - GtkWidget *widget; - struct ical calendars[128]; - int ncalendars; - - struct event events[MAX_EVENTS]; - int nevents; - - enum cal_flags flags; - // TODO: make multiple target selection - struct event *target; - int minute_round; - int refresh_events; - int x, y, mx, my; - int gutter_height; - double zoom, zoom_at; - - time_t view_start; - time_t view_end; - - int height, width; -}; - -struct extra_data { - GtkWindow *win; - struct cal *cal; -}; - -static GdkCursor *cursor_default; -static GdkCursor *cursor_pointer; -static icaltimezone *g_timezone; -static int g_lmargin = 18; -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 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; - time_t today; - int start_at = 0; - struct tm nowtm; - - now = time(NULL); - nowtm = *localtime(&now); - nowtm.tm_hour = 0; - nowtm.tm_min = 0; - today = mktime(&nowtm); - - cal->gutter_height = 40; - cal->minute_round = 30; - cal->ncalendars = 0; - cal->nevents = 0; - cal->view_end = today + DAY_SECONDS; - cal->view_start = today + start_at; - cal->x = g_lmargin; - cal->y = cal->gutter_height; - cal->zoom = 1.0; -} - -static void -on_change_view(struct cal *cal) { - events_for_view(cal, cal->view_start, cal->view_end); -} - -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; - - calendar_print_state(cal); - gtk_widget_queue_draw(widget); - - return 1; -} - -static char * -file_load(char *path) { - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - - char *string = malloc(fsize); - int res = fread(string, fsize, 1, f); - if (!res) return NULL; - fclose(f); - 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 - struct ical* ical; - - // TODO: free icalcomponent somewhere - const char *str = file_load(path); - if (str == NULL) return NULL; - icalcomponent *calendar = icalparser_parse_string(str); - if (!calendar) return NULL; - - // TODO: support >128 calendars - if (length(cal->calendars) == cal->ncalendars) - return NULL; - - ical = &cal->calendars[cal->ncalendars++]; - ical->calendar = calendar; - - free((void*)str); - return ical; -} - - -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 -calendar_drop(struct cal *cal, double mx, double my) { - struct event *ev = cal->target; - if (ev) { - icaltime_span span = icalcomponent_get_span(ev->vevent); - - // TODO: use default event length when dragging from gutter? - time_t len = span.end - span.start; - - // XXX: should dragging timezone be the local timezone? - // XXX: this will probably destroy the timezone, we don't want that - // TODO: convert timezone on drag? - - icaltimetype startt = - icaltime_from_timet(ev->drag_time, 0); - - icalcomponent_set_dtstart(ev->vevent, startt); - - icaltimetype endt = - icaltime_from_timet(ev->drag_time + len, 0); - - icalcomponent_set_dtend(ev->vevent, endt); - } -} - -static void -event_click(struct cal *cal, struct event *event, int mx, int my) { - printf("clicked %s\n", icalcomponent_get_summary(event->vevent)); - - calendar_loc_to_time(cal, my); -} - -static icalcomponent * -event_create(time_t time) { - icalcomponent *vevent; - vevent = icalcomponent_new(ICAL_VEVENT_COMPONENT); - return vevent; -} - -static icalcomponent * -calendar_def_cal(struct cal *cal) { - // TODO: configurable default calendar - if (cal->ncalendars > 0) - return cal->calendars[0].calendar; - return NULL; -} - -static void -calendar_refresh_events(struct cal *cal) { - cal->refresh_events = 1; - gtk_widget_queue_draw(cal->widget); -} - -static void -calendar_view_clicked(struct cal *cal, int mx, int my) { - icalcomponent *vevent; - time_t closest; - int y; - char buf[32]; - - closest = closest_timeblock(cal, my); - - icaltimetype dtstart = icaltime_from_timet(closest, 0); - icaltimetype dtend = icaltime_from_timet(closest + cal->minute_round * 60, 0); - - format_locale_timet(buf, length(buf), closest); - printf("(%d,%d) clicked @%s\n", mx, my, buf); - vevent = event_create(closest); - icalcomponent_set_summary(vevent, "New Event"); - // TODO: configurable default duration - /* icalcomponent_set_duration(vevent, duration); */ - icalcomponent_set_dtstart(vevent, dtstart); - icalcomponent_set_dtend(vevent, dtend); - icalcomponent_add_component(calendar_def_cal(cal), vevent); - calendar_refresh_events(cal); - - y = calendar_time_to_loc(cal, closest) * cal->height; -} - -static int -on_press(GtkWidget *widget, GdkEventButton *ev, gpointer user_data) { - struct extra_data *data = (struct extra_data*)user_data; - struct cal *cal = data->cal; - double mx = ev->x; - double my = ev->y; - int state_changed = 1; - - switch (ev->type) { - case GDK_BUTTON_PRESS: - cal->flags |= CAL_MDOWN; - cal->target = events_hit(cal->events, cal->nevents, mx, my); - break; - case GDK_BUTTON_RELEASE: - if ((cal->flags & CAL_DRAGGING) != 0) { - // finished drag - // TODO: handle drop into and out of gutter - calendar_drop(cal, mx, my); - } - else { - // clicked target - if (cal->target) - event_click(cal, cal->target, mx, my); - else if (my < cal->y) { - // TODO: gutter clicked, create date event + increase gutter size - } - else { - calendar_view_clicked(cal, mx, my - cal->y); - } - } - // finished dragging - cal->flags &= ~(CAL_MDOWN | CAL_DRAGGING); - - // clear target drag state - if (cal->target) { - cal->target->dragx = 0.0; - cal->target->dragy = 0.0; - cal->target->drag_time = - icaltime_as_timet(icalcomponent_get_dtstart(cal->target->vevent)); - cal->target = NULL; - } - break; - default: state_changed = 0; break; - } - - if (state_changed) - on_state_change(widget, (GdkEvent*)ev, user_data); - - return 1; -} - -static struct event* -event_any_flags(struct event *events, int nevents, int flag) { - for (int i = 0; i < nevents; i++) { - if ((events[i].flags & flag) != 0) - return &events[i]; - } - 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 - // https://developer.gnome.org/gtk3/stable/GtkGestureZoom.html - struct extra_data *data = (struct extra_data*)user_data; - struct cal *cal = data->cal; - double newzoom = cal->zoom - ev->delta_y * 0.1; - - if (newzoom < ZOOM_MIN) { - newzoom = ZOOM_MIN; - } - else if (newzoom > ZOOM_MAX) { - newzoom = ZOOM_MAX; - } - - cal->zoom = newzoom; - cal->zoom_at = cal->my; - - on_state_change(widget, (GdkEvent*)ev, user_data); - - return 0; -} - -static int -on_motion(GtkWidget *widget, GdkEventMotion *ev, gpointer user_data) { - static struct event* prev_hit = NULL; - - 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); - - 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; - } - - // 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; - } - } - - events_update_flags (cal->events, cal->nevents, mx, my); - hit = event_any_flags(cal->events, cal->nevents, EV_HIGHLIGHTED); - - gdk_window_set_cursor(gdkwin, hit ? cursor_pointer : cursor_default); - - state_changed = dragging_event || hit != prev_hit; - - fflush(stdout); - prev_hit = hit; - - if (state_changed) - on_state_change(widget, (GdkEvent*)ev, user_data); - - return 1; -} - -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); - } -} - - -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; - - if (!margin_calculated) { - char buffer[32]; - cairo_text_extents_t exts; - - 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; - - margin_calculated = 1; - } - - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); - - gtk_window_get_size(data->win, &width, &height); - - cal->y = cal->gutter_height; - - cal->width = width - cal->x; - cal->height = height - cal->y; - - calendar_update(cal); - calendar_draw(cr, cal); - - return FALSE; -} - - -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 -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; - - cairo_set_line_width(cr, 1.0); - - 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); */ - - /* 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); -} - - -// 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_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); -} - -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 -format_locale_time(char *buffer, int bsize, struct tm *tm) { - strftime(buffer, bsize, "%X", tm); - time_remove_seconds(buffer, 2); -} - - -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); - } - } -} - -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 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 time_t -location_to_time(time_t start, time_t end, double loc) { - return (time_t)((double)start) + (loc * (end - start)); -} - -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); - - double dloc = eloc - sloc; - eheight = dloc * height; - y = (sloc * height) + sy; - } - - ev->width = width; - ev->height = eheight; - ev->x = sx; - ev->y = y; -} - -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); -} - -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; - } - - // 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", 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 -calendar_update (struct cal *cal) { - int i, width, height; - width = cal->width; - height = cal->height; - - width -= cal->x; - height -= cal->y * 2; - - 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); - } -} - -static int -calendar_draw (cairo_t *cr, struct cal *cal) { - int i, width, height; - time_t now; - width = cal->width; - height = cal->height; - - cairo_move_to(cr, cal->x, cal->y); - draw_background(cr, width, height); - draw_hours(cr, cal); - - // draw calendar events - for (i = 0; i < cal->nevents; ++i) { - struct event *ev = &cal->events[i]; - event_draw(cr, cal, ev); - } - - draw_time_line(cr, cal, time(&now)); - - return 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; - - 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; - - calendar_create(&cal); - - ical = calendar_load_ical(&cal, "/home/jb55/var/ical2org/personal.ical"); - if (ical) - ical->color = defcol; - - ical = calendar_load_ical(&cal, "/home/jb55/var/ical2org/monstercat.ical"); - if (ical) { - ical->color = defcol; - ical->color.r *= 0.5; - } - - 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); - - // 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; - - color.red = BGCOLOR * 0xffff * 0.6; - color.green = BGCOLOR * 0xffff * 0.6; - color.blue = BGCOLOR * 0xffff * 0.6; - - /* setlocale(LC_TIME, ""); */ - - // calc margin - format_margin_time(buffer, 32, 12); - - gtk_init(&argc, &argv); - - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - - 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); - - 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), "draw", - G_CALLBACK(on_draw_event), (gpointer)&extra_data); - - 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_modify_bg(window, GTK_STATE_NORMAL, &color); - gtk_widget_show_all(window); - - gtk_main(); - - return 0; -} diff --git a/viscal.c b/viscal.c @@ -0,0 +1,987 @@ + +#include <cairo/cairo.h> +#include <gtk/gtk.h> +#include <libical/ical.h> +#include <assert.h> +#include <time.h> +#include <string.h> +#include <stdlib.h> +#include <math.h> +#include <locale.h> + +#define length(array) (sizeof((array))/sizeof((array)[0])) + +#define max(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; }) + +#define min(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; }) + +#define MAX_EVENTS 1024 + +static const double BGCOLOR = 0.35; +static const int DAY_SECONDS = 86400; +static const int TXTPAD = 11; +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; */ + + +enum event_flags { + EV_SELECTED = 1 << 0 + , EV_HIGHLIGHTED = 1 << 1 + , EV_DRAGGING = 1 << 2 +}; + +enum cal_flags { + CAL_MDOWN = 1 << 0 + , CAL_DRAGGING = 1 << 1 +}; + +union rgba { + double rgba[4]; + struct { + double r, g, b, a; + }; +}; + +struct ical { + icalcomponent * calendar; + union rgba color; +}; + +struct event { + icalcomponent *vevent; + struct ical *ical; + + enum event_flags flags; + // set on draw + double width, height; + double x, y; + double dragx, dragy; + time_t drag_time; +}; + +struct cal { + GtkWidget *widget; + struct ical calendars[128]; + int ncalendars; + + struct event events[MAX_EVENTS]; + int nevents; + + enum cal_flags flags; + // TODO: make multiple target selection + struct event *target; + int minute_round; + int refresh_events; + int x, y, mx, my; + int gutter_height; + double zoom, zoom_at; + + time_t view_start; + time_t view_end; + + int height, width; +}; + +struct extra_data { + GtkWindow *win; + struct cal *cal; +}; + +static GdkCursor *cursor_default; +static GdkCursor *cursor_pointer; +static icaltimezone *g_timezone; +static int g_lmargin = 18; +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 const double dashed[] = {1.0}; + +static void +calendar_create(struct cal *cal) { + time_t now; + time_t today; + int start_at = 0; + struct tm nowtm; + + now = time(NULL); + nowtm = *localtime(&now); + nowtm.tm_hour = 0; + nowtm.tm_min = 0; + today = mktime(&nowtm); + + cal->gutter_height = 40; + cal->minute_round = 30; + cal->ncalendars = 0; + cal->nevents = 0; + cal->view_end = today + DAY_SECONDS; + cal->view_start = today + start_at; + cal->x = g_lmargin; + cal->y = cal->gutter_height; + 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; + + calendar_print_state(cal); + gtk_widget_queue_draw(widget); + + return 1; +} + + +static char * +file_load(char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + + char *string = malloc(fsize); + int res = fread(string, fsize, 1, f); + if (!res) return NULL; + fclose(f); + return string; +} + +static struct ical * +calendar_load_ical(struct cal *cal, char *path) { + // TODO: don't load duplicate calendars + struct ical* ical; + + // TODO: free icalcomponent somewhere + const char *str = file_load(path); + if (str == NULL) return NULL; + icalcomponent *calendar = icalparser_parse_string(str); + if (!calendar) return NULL; + + // TODO: support >128 calendars + if (length(cal->calendars) == cal->ncalendars) + return NULL; + + ical = &cal->calendars[cal->ncalendars++]; + ical->calendar = calendar; + + free((void*)str); + return ical; +} + + +/* 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 +calendar_drop(struct cal *cal, double mx, double my) { + struct event *ev = cal->target; + if (ev) { + icaltime_span span = icalcomponent_get_span(ev->vevent); + + // TODO: use default event length when dragging from gutter? + time_t len = span.end - span.start; + + // XXX: should dragging timezone be the local timezone? + // XXX: this will probably destroy the timezone, we don't want that + // TODO: convert timezone on drag? + + icaltimetype startt = + icaltime_from_timet(ev->drag_time, 0); + + icalcomponent_set_dtstart(ev->vevent, startt); + + icaltimetype endt = + icaltime_from_timet(ev->drag_time + len, 0); + + icalcomponent_set_dtend(ev->vevent, endt); + } +} + + +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)); + + calendar_loc_to_time(cal, my); +} + + +static icalcomponent * +event_create(time_t time) { + icalcomponent *vevent; + vevent = icalcomponent_new(ICAL_VEVENT_COMPONENT); + return vevent; +} + +static icalcomponent * +calendar_def_cal(struct cal *cal) { + // TODO: configurable default calendar + if (cal->ncalendars > 0) + return cal->calendars[0].calendar; + return NULL; +} + +static void +calendar_refresh_events(struct cal *cal) { + cal->refresh_events = 1; + 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; */ + char buf[32]; + + closest = closest_timeblock(cal, my); + + icaltimetype dtstart = icaltime_from_timet(closest, 0); + icaltimetype dtend = icaltime_from_timet(closest + cal->minute_round * 60, 0); + + format_locale_timet(buf, length(buf), closest); + printf("(%d,%d) clicked @%s\n", mx, my, buf); + vevent = event_create(closest); + icalcomponent_set_summary(vevent, "New Event"); + // TODO: configurable default duration + /* icalcomponent_set_duration(vevent, duration); */ + icalcomponent_set_dtstart(vevent, dtstart); + icalcomponent_set_dtend(vevent, dtend); + icalcomponent_add_component(calendar_def_cal(cal), vevent); + calendar_refresh_events(cal); + + /* 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; + struct cal *cal = data->cal; + double mx = ev->x; + double my = ev->y; + int state_changed = 1; + + switch (ev->type) { + case GDK_BUTTON_PRESS: + cal->flags |= CAL_MDOWN; + cal->target = events_hit(cal->events, cal->nevents, mx, my); + break; + case GDK_BUTTON_RELEASE: + if ((cal->flags & CAL_DRAGGING) != 0) { + // finished drag + // TODO: handle drop into and out of gutter + calendar_drop(cal, mx, my); + } + else { + // clicked target + if (cal->target) + event_click(cal, cal->target, mx, my); + else if (my < cal->y) { + // TODO: gutter clicked, create date event + increase gutter size + } + else { + calendar_view_clicked(cal, mx, my - cal->y); + } + } + // finished dragging + cal->flags &= ~(CAL_MDOWN | CAL_DRAGGING); + + // clear target drag state + if (cal->target) { + cal->target->dragx = 0.0; + cal->target->dragy = 0.0; + cal->target->drag_time = + icaltime_as_timet(icalcomponent_get_dtstart(cal->target->vevent)); + cal->target = NULL; + } + break; + default: state_changed = 0; break; + } + + if (state_changed) + on_state_change(widget, (GdkEvent*)ev, user_data); + + return 1; +} + +static struct event* +event_any_flags(struct event *events, int nevents, int flag) { + for (int i = 0; i < nevents; i++) { + if ((events[i].flags & flag) != 0) + return &events[i]; + } + return NULL; +} + +static int +on_scroll(GtkWidget *widget, GdkEventScroll *ev, gpointer user_data) { + // TODO: GtkGestureZoom + // https://developer.gnome.org/gtk3/stable/GtkGestureZoom.html + struct extra_data *data = (struct extra_data*)user_data; + struct cal *cal = data->cal; + double newzoom = cal->zoom - ev->delta_y * 0.1; + + if (newzoom < ZOOM_MIN) { + newzoom = ZOOM_MIN; + } + else if (newzoom > ZOOM_MAX) { + newzoom = ZOOM_MAX; + } + + cal->zoom = newzoom; + cal->zoom_at = cal->my; + + on_state_change(widget, (GdkEvent*)ev, user_data); + + return 0; +} + +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); + } +} + + + +static int +on_motion(GtkWidget *widget, GdkEventMotion *ev, gpointer user_data) { + static struct event* prev_hit = NULL; + + 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); + + 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; + + // 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; + } + } + + events_update_flags (cal->events, cal->nevents, mx, my); + hit = event_any_flags(cal->events, cal->nevents, EV_HIGHLIGHTED); + + gdk_window_set_cursor(gdkwin, hit ? cursor_pointer : cursor_default); + + state_changed = dragging_event || hit != prev_hit; + + fflush(stdout); + prev_hit = hit; + + if (state_changed) + on_state_change(widget, (GdkEvent*)ev, user_data); + + return 1; +} + + +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); +} + + +static double +time_to_location (time_t start, time_t end, time_t time) { + return ((double)(time - start) / ((double)(end - start))); +} + + + +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 +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 +calendar_update (struct cal *cal) { + int i, width, height; + width = cal->width; + height = cal->height; + + width -= cal->x; + height -= cal->y * 2; + + 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); + } +} + + + + +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); +} + + +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 +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 +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 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_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; + + cairo_set_line_width(cr, 1.0); + + 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); */ + + /* cairo_set_source_rgb (cr, 0, 0, 0); */ + /* draw_line(cr, cal->x, y + 1, w); */ + /* cairo_stroke(cr); */ +} + + +static int +calendar_draw (cairo_t *cr, struct cal *cal) { + int i, width, height; + time_t now; + width = cal->width; + height = cal->height; + + cairo_move_to(cr, cal->x, cal->y); + draw_background(cr, width, height); + draw_hours(cr, cal); + + // draw calendar events + for (i = 0; i < cal->nevents; ++i) { + struct event *ev = &cal->events[i]; + event_draw(cr, cal, ev); + } + + draw_time_line(cr, cal, time(&now)); + + return 1; +} + +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; + + if (!margin_calculated) { + char buffer[32]; + cairo_text_extents_t exts; + + 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; + + margin_calculated = 1; + } + + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); + + gtk_window_get_size(data->win, &width, &height); + + cal->y = cal->gutter_height; + + cal->width = width - cal->x; + cal->height = height - cal->y; + + calendar_update(cal); + calendar_draw(cr, cal); + + return FALSE; +} + + +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; + + 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; + + calendar_create(&cal); + + if (argc < 2) + usage(); + + printf("loading calendar %s\n", argv[1]); + ical = calendar_load_ical(&cal, argv[1]); + ical->color = defcol; + + on_change_view(&cal); + + // 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; + + color.red = BGCOLOR * 0xffff * 0.6; + color.green = BGCOLOR * 0xffff * 0.6; + color.blue = BGCOLOR * 0xffff * 0.6; + + /* setlocale(LC_TIME, ""); */ + + // calc margin + format_margin_time(buffer, 32, 12); + + gtk_init(&argc, &argv); + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + + 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); + + 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), "draw", + G_CALLBACK(on_draw_event), (gpointer)&extra_data); + + 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_modify_bg(window, GTK_STATE_NORMAL, &color); + gtk_widget_show_all(window); + + gtk_main(); + + return 0; +}