details:   https://code.tryton.org/goocalendar/commit/54b87bf46b5f
branch:    default
user:      Bastian Germann <[email protected]>
date:      Mon May 04 11:12:23 2026 +0200
description:
        Remove Goocanvas

        Closes #38
diffstat:

 CHANGELOG                |    1 +
 COPYRIGHT                |    1 +
 README.rst               |    2 +-
 doc/index.rst            |   18 +-
 goocalendar/__init__.py  |    5 +-
 goocalendar/_calendar.py |  977 +++++++++++++++++++++++++---------------------
 pyproject.toml           |    4 +-
 7 files changed, 550 insertions(+), 458 deletions(-)

diffs (1592 lines):

diff -r c9090b2733e3 -r 54b87bf46b5f CHANGELOG
--- a/CHANGELOG Sun Mar 29 08:50:20 2026 +0200
+++ b/CHANGELOG Mon May 04 11:12:23 2026 +0200
@@ -1,3 +1,4 @@
+* Remove GooCanvas
 * Remove support for Python older than 3.9
 * Upgrade to pyproject
 
diff -r c9090b2733e3 -r 54b87bf46b5f COPYRIGHT
--- a/COPYRIGHT Sun Mar 29 08:50:20 2026 +0200
+++ b/COPYRIGHT Mon May 04 11:12:23 2026 +0200
@@ -1,3 +1,4 @@
+Copyright (C) 2026 Bastian Germann <[email protected]>
 Copyright (C) 2012 Antoine Smolders
 Copyright (C) 2012-2022 Cédric Krier <[email protected]>
 Copyright (C) 2007 Samuel Abels <http://debain.org>
diff -r c9090b2733e3 -r 54b87bf46b5f README.rst
--- a/README.rst        Sun Mar 29 08:50:20 2026 +0200
+++ b/README.rst        Mon May 04 11:12:23 2026 +0200
@@ -1,7 +1,7 @@
 GooCalendar
 ===========
 
-A calendar widget for GTK using GooCanvas
+A calendar widget for GTK
 
 Nutshell
 --------
diff -r c9090b2733e3 -r 54b87bf46b5f doc/index.rst
--- a/doc/index.rst     Sun Mar 29 08:50:20 2026 +0200
+++ b/doc/index.rst     Mon May 04 11:12:23 2026 +0200
@@ -1,25 +1,25 @@
-:mod:`goocalendar` --- Calendar widget using GooCanvas
-======================================================
+:mod:`goocalendar` --- Calendar widget
+======================================
 
 .. module:: goocalendar
-   :synopsis: Calendar widget using GooCanvas
+   :synopsis: Calendar widget
 .. moduleauthor:: Samuel Abels <http://debain.org>
 .. moduleauthor:: Cédric Krier <[email protected]>
 .. moduleauthor:: Antoine Smolders <[email protected]>
 .. sectionauthor:: Antoine Smolders <[email protected]>
 
-The :mod:`goocalendar <goocalendar>` module supplies a calendar widget drawed
-with GooCanvas that can display a month view and a week view. It also supplies
-classes to manage events you can add to the calendar.
+The :mod:`goocalendar <goocalendar>` module supplies a calendar widget that can
+display a month view and a week view.
+It also supplies classes to manage events you can add to the calendar.
 
 
 .. _calendar:
 
 Calendar Objects
 ------------------
-A :class:`Calendar <goocalendar.Calendar>` is a calendar widget using
-GooCanvas that can display a month view and a week view. It holds an
-:class:`EventStore<goocalendar.EventStore>` which contains events
+A :class:`Calendar <goocalendar.Calendar>` is a calendar widget that can
+display a month view and a week view.
+It holds an :class:`EventStore<goocalendar.EventStore>` which contains events
 displayed in the calendar.
 
 .. class:: goocalendar.Calendar([event_store[, view[, time_format[, \
diff -r c9090b2733e3 -r 54b87bf46b5f goocalendar/__init__.py
--- a/goocalendar/__init__.py   Sun Mar 29 08:50:20 2026 +0200
+++ b/goocalendar/__init__.py   Mon May 04 11:12:23 2026 +0200
@@ -4,10 +4,7 @@
 
 gi.require_version('Gtk', '3.0')
 gi.require_version('Gdk', '3.0')
-try:
-    gi.require_version('GooCanvas', '3.0')
-except ValueError:
-    gi.require_version('GooCanvas', '2.0')
+gi.require_version('PangoCairo', '1.0')
 from ._calendar import Calendar  # noqa: E402
 from ._event import Event, EventStore  # noqa: E402
 
diff -r c9090b2733e3 -r 54b87bf46b5f goocalendar/_calendar.py
--- a/goocalendar/_calendar.py  Sun Mar 29 08:50:20 2026 +0200
+++ b/goocalendar/_calendar.py  Mon May 04 11:12:23 2026 +0200
@@ -2,14 +2,13 @@
 # this repository contains the full copyright notices and license terms.
 import calendar
 import datetime
-import math
 
-from gi.repository import Gdk, GObject, GooCanvas, Gtk, Pango
+from gi.repository import Gdk, GObject, Gtk, Pango, PangoCairo
 
 from . import util
 
 
-class Calendar(GooCanvas.Canvas):
+class Calendar(Gtk.DrawingArea):
     AVAILABLE_VIEWS = ["month", "week", "day"]
     MIN_PER_LEVEL = 15  # Number of minutes per graduation for drag and drop
 
@@ -75,14 +74,16 @@
         self._day_width = 0
         self._day_height = 0
         self._event_items = []
+        self._tooltip_text = None
+        self._tooltip_x = None
+        self._tooltip_y = None
         assert view in self.AVAILABLE_VIEWS
         self.view = view
         self.selected_date = datetime.date.today()
         self.time_format = time_format
         self.min_width = self.min_height = 200
-        self.set_bounds(0, 0, self.min_width, self.min_height)
         self.set_can_focus(True)
-        self.set_events(
+        self.add_events(
             Gdk.EventMask.EXPOSURE_MASK
             | Gdk.EventMask.BUTTON_PRESS_MASK
             | Gdk.EventMask.BUTTON_RELEASE_MASK
@@ -93,24 +94,23 @@
             | Gdk.EventMask.ENTER_NOTIFY_MASK
             | Gdk.EventMask.LEAVE_NOTIFY_MASK
             | Gdk.EventMask.FOCUS_CHANGE_MASK)
-        self.connect_after('realize', self.on_realize)
+        self.connect('realize', self.on_realize)
         self.connect('size-allocate', self.on_size_allocate)
         self.connect('key-press-event', self.on_key_press_event)
+        self.connect('draw', self.on_draw)
+        self.connect('button-press-event', self.on_button_press_event)
+        self.connect('button-release-event', self.on_button_release_event)
+        self.connect('motion-notify-event', self.on_motion_notify_event)
+        self.connect('query-tooltip', self.on_query_tooltip)
 
-        # Initialize background, timeline and days and add them to canvas
-        root = self.get_root_item()
-        self._bg_rect = GooCanvas.CanvasRect(parent=root, x=0, y=0,
-            stroke_color='white', fill_color='white')
-        self._timeline = TimelineItem(self, time_format=self.time_format)
-        root.add_child(self._timeline, -1)
+        # Initialize day and event data structures
         self.days = []
         while len(self.days) < 42:  # 6 rows of 7 days
             box = DayItem(self)
-            root.add_child(box, -1)
-            box.connect('button_press_event',
-                self.on_day_item_button_press_event)
             self.days.append(box)
 
+        self._timeline = TimelineItem(self, time_format=self.time_format)
+
     def do_set_property(self, prop, value):
         self.__props[prop.name] = value
 
@@ -182,11 +182,10 @@
         new_day.full_border = True
 
         # Redraw.
-        old_day.update()
-        new_day.update()
         self._selected_day = new_day
         if old_day != new_day:
             self.emit('day-selected', self.selected_date)
+        self.queue_draw()
 
     def previous_page(self):
         cal = calendar.Calendar(self.firstweekday)
@@ -235,60 +234,57 @@
 
     def on_realize(self, *args):
         self._realized = True
-        self.grab_focus(self.get_root_item())
+        self.grab_focus()
         self.on_size_allocate(*args)
 
     def on_size_allocate(self, *args):
         alloc = self.get_allocation()
         if not self._realized or alloc.width < 10 or alloc.height < 10:
             return
-        self.set_bounds(0, 0, alloc.width, alloc.height)
         self.update()
 
     def update(self):
         if not self._realized:
             return
         min_size = (self.min_width, self.min_height)
-        self.draw_background()
-        if self.view == "month":
-            self.draw_month()
-        elif self.view == "week":
-            self.draw_week()
-        elif self.view == "day":
-            self.draw_day()
-        self.draw_events()
+        self._prepare_layout()
         if min_size != (self.min_width, self.min_height):
             self.queue_resize()
+        self.queue_draw()
 
-    def draw_background(self):
-        x, y, w, h = self.get_bounds()
-        self._bg_rect.set_property('width', w)
-        self._bg_rect.set_property('height', h)
+    def _prepare_layout(self):
+        """Prepare the layout data for drawing."""
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
 
-    def draw_day(self):
+        if self.view == "month":
+            self._prepare_month_layout(w, h)
+        elif self.view == "week":
+            self._prepare_week_layout(w, h)
+        elif self.view == "day":
+            self._prepare_day_layout(w, h)
+        self._prepare_events()
+
+    def _prepare_day_layout(self, w, h):
         """
-        Draws the currently selected day.
+        Prepares the layout for the currently selected day.
         """
-        x, y, w, h = self.get_bounds()
-        timeline_w = self._timeline.width
+        timeline_w = self._timeline.get_width(self)
         dayno = self.selected_date.weekday()
         day_name = calendar.day_name[dayno]
-        # Sum the needed space for the date before the day_name
         caption_size = len(day_name) + 3
         day_width_min = caption_size * self.font_size / Pango.SCALE
         day_width_max = (w - timeline_w)
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h
 
-        # Redraw all days.
+        # Prepare all days.
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
-            # Hide all days that are not part of the current day
             for i, date in enumerate(week):
                 box = self.days[weekno * 7 + i]
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
+                box.visible = False
             if self.selected_date not in week:
                 continue
 
@@ -297,7 +293,6 @@
             else:
                 the_body_color = self.props.body_color
 
-            # Draw.
             box = self.days[weekno * 7 + dayno]
             box.x = timeline_w
             box.y = 0
@@ -309,19 +304,17 @@
             box.border_color = self.props.selected_border_color
             box.body_color = the_body_color
             box.title_text_color = self.props.selected_text_color
-            box.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-            box.update()
+            box.visible = True
 
         self.min_width = int(timeline_w + day_width_min)
-        self.min_height = int((24 + 1) * self._timeline.min_line_height)
+        self.min_height = int(
+            (24 + 1) * self._timeline.get_min_line_height(self))
 
-    def draw_week(self):
+    def _prepare_week_layout(self, w, h):
         """
-        Draws the currently selected week.
+        Prepares the layout for the currently selected week.
         """
-        x, y, w, h = self.get_bounds()
-        timeline_w = self._timeline.width
+        timeline_w = self._timeline.get_width(self)
         caption_size = max(len(day_name) for day_name in calendar.day_name)
         caption_size += 3  # The needed space for the date before the day_name
         day_width_min = caption_size * self.font_size / Pango.SCALE
@@ -329,21 +322,16 @@
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h
 
-        # Redraw all days.
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
-            # Hide all days that are not part of the current week.
             if self.selected_date not in week:
                 for dayno, date in enumerate(week):
                     box = self.days[weekno * 7 + dayno]
-                    box.set_property(
-                        'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
+                    box.visible = False
                 continue
 
-            # Draw the days that are part of the current week.
             for dayno, current_date in enumerate(week):
-                # Highlight the day according to it's selection.
                 selected = current_date == self.selected_date
                 if selected:
                     the_border_color = self.props.selected_border_color
@@ -356,7 +344,6 @@
                 else:
                     the_body_color = self.props.body_color
 
-                # Draw.
                 box = self.days[weekno * 7 + dayno]
                 box.x = self._day_width * dayno + timeline_w
                 box.y = 0
@@ -368,22 +355,19 @@
                 box.border_color = the_border_color
                 box.body_color = the_body_color
                 box.title_text_color = the_text_color
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-                box.update()
+                box.visible = True
 
                 if selected:
                     self._selected_day = box
-                    self._line_height = self._selected_day.line_height
 
         self.min_width = int(timeline_w + 7 * day_width_min)
-        self.min_height = int((24 + 1) * self._timeline.min_line_height)
+        self.min_height = int(
+            (24 + 1) * self._timeline.get_min_line_height(self))
 
-    def draw_month(self):
+    def _prepare_month_layout(self, w, h):
         """
-        Draws the currently selected month.
+        Prepares the layout for the currently selected month.
         """
-        x1, y1, w, h = self.get_bounds()
         caption_size = max(len(day_name) for day_name in calendar.day_name)
         caption_size += 3  # The needed space for the date before the day_name
         day_width_min = caption_size * self.font_size / Pango.SCALE
@@ -391,19 +375,11 @@
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h / 6
 
-        # Hide the timeline.
-        if self._timeline is not None:
-            self._timeline.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
-
-        # Draw the grid.
         y_pos = 0
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
             for dayno, date in enumerate(week):
-                # The color depends on whether each day is part of the
-                # current month.
                 if (not util.same_month(date, self.selected_date)):
                     the_border_color = self.props.inactive_border_color
                     the_text_color = self.props.inactive_text_color
@@ -411,7 +387,6 @@
                     the_border_color = self.props.border_color
                     the_text_color = self.props.text_color
 
-                # Highlight the day according to it's selection.
                 selected = date == self.selected_date
                 if selected:
                     the_border_color = self.props.selected_border_color
@@ -421,7 +396,6 @@
                 else:
                     the_body_color = self.props.body_color
 
-                # Draw a box for the day.
                 box = self.days[weekno * 7 + dayno]
                 box.x = self._day_width * dayno
                 box.y = y_pos
@@ -433,18 +407,16 @@
                 box.body_color = the_body_color
                 box.title_text_color = the_text_color
                 box.type = 'month'
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-                box.update()
+                box.visible = True
 
                 if selected:
                     self._selected_day = box
-                    self._line_height = self._selected_day.line_height
 
             y_pos += self._day_height
 
         self.min_width = int(7 * day_width_min)
-        self.min_height = int((6 * 2 + 1) * self._timeline.min_line_height)
+        self.min_height = int(
+            (6 * 2 + 1) * self._timeline.get_min_line_height(self))
 
     def _get_day_item(self, find_date):
         cal = calendar.Calendar(self.firstweekday)
@@ -492,16 +464,18 @@
                 return line
         return None
 
-    def draw_events(self):
-        _, _, bound_width, _ = self.get_bounds()
+    def _prepare_events(self):
+        alloc = self.get_allocation()
+        bound_width = alloc.width
+
         # Clear previous events.
-        for item in self._event_items:
-            item.remove()
         self._event_items = []
         for day in self.days:
             day.lines.clear()
             day.show_indic = False
-            day.update()
+            # Compute line height for each visible day
+            if day.visible:
+                day.compute_line_height(self)
 
         if not self._event_store:
             return
@@ -532,7 +506,6 @@
         non_all_day_events = []
         for event in events:
             event.event_items = []
-            # Handle non-all-day events differently in week and day modes.
             if (self.view in {"week", "day"} and not event.all_day
                     and not event.multidays):
                 non_all_day_events.append(event)
@@ -547,7 +520,6 @@
             if free_line is None:
                 for day in days:
                     day.show_indic = True
-                    day.update()
                 continue
 
             max_line_height = max(x.line_height for x in days)
@@ -580,17 +552,10 @@
                 if len(event.event_items):
                     event_item.no_caption = True
                 event.event_items.append(event_item)
-                event_item.connect('button_press_event',
-                    self.on_event_item_button_press_event)
-                event_item.connect('button_release_event',
-                    self.on_event_item_button_release)
-                event_item.connect('motion_notify_event',
-                    self.on_event_item_motion_notified)
                 self._event_items.append(event_item)
-                self.get_root_item().add_child(event_item, -1)
                 if self.view == "day":
-                    x_start = self._timeline.width
-                    width = bound_width - self._timeline.width
+                    x_start = self._timeline.get_width(self)
+                    width = bound_width - self._timeline.get_width(self)
                 else:
                     x_start = day.x
                     width = (day.width + 2) * len(week)
@@ -619,32 +584,33 @@
                     event_item.x += 2
                     event_item.width -= 6
                     event_item.type = 'leftright'
-                event_item.update()
+
         # Add the day title
         if self._selected_day:
             max_y += self._selected_day.line_height
+            self._line_height = self._selected_day.line_height
 
         if self.view == "month":
             return
 
-        # Redraw the timeline.
-        self._timeline.set_property(
-            'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-        x, y, w, h = self.get_bounds()
+        # Prepare the timeline.
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
         max_y += (h - max_y) % 24
-        self._timeline.x = x
+        self._timeline.x = 0
         self._timeline.y = max_y
         self._timeline.height = h - max_y
         self._timeline.line_color = self.props.body_color
         self._timeline.bg_color = self.props.border_color
         self._timeline.text_color = self.props.text_color
-        self._timeline.update()
-        min_line_height = self._timeline.min_line_height
-        line_height = self._timeline.line_height
+        self._timeline.visible = True
+
+        min_line_height = self._timeline.get_min_line_height(self)
+        line_height = self._timeline.get_line_height(self)
         self.minute_height = line_height / 60.0
         self.min_height = int(max_y + 24 * min_line_height)
 
-        # Draw non-all-day events.
+        # Prepare non-all-day events.
         for date in dates:
             date_start = datetime.datetime.combine(date, datetime.time())
             date_end = (datetime.datetime.combine(date_start, datetime.time())
@@ -654,9 +620,7 @@
                 date_start, date_end)
             day_events.sort()
             columns = []
-            column = 0
 
-            # Sort events into columns.
             remaining_events = day_events[:]
             while len(remaining_events) > 0:
                 columns.append([remaining_events[0]])
@@ -668,17 +632,14 @@
                 for event in columns[-1]:
                     remaining_events.remove(event)
 
-            # Walk through all columns.
             for columnno, column in enumerate(columns):
                 for event in column:
-                    # Crop the event to the current day.
                     event1_start = max(event.start, date_start)
                     event1_end = min(event.end, date_end)
 
                     parallel = util.count_parallel_events(day_events,
                         event1_start, event1_end)
 
-                    # Draw.
                     top_offset = event1_start - date_start
                     bottom_offset = event1_end - event1_start
                     top_offset_mins = top_offset.seconds / 60
@@ -690,19 +651,13 @@
                     if event.event_items:
                         event_item.no_caption = True
                     event.event_items.append(event_item)
-                    event_item.connect('button_press_event',
-                        self.on_event_item_button_press_event)
-                    event_item.connect('button_release_event',
-                        self.on_event_item_button_release)
-                    event_item.connect('motion_notify_event',
-                        self.on_event_item_motion_notified)
                     self._event_items.append(event_item)
-                    self.get_root_item().add_child(event_item, -1)
                     y_off1 = top_offset_mins * self.minute_height
                     y_off2 = bottom_offset_mins * self.minute_height
                     if self.view == "day":
-                        x_start = self._timeline.width
-                        column_width = (w - self._timeline.width) / parallel
+                        x_start = self._timeline.get_width(self)
+                        column_width = (
+                            (w - self._timeline.get_width(self)) / parallel)
                     else:
                         column_width = day.width / parallel
                         x_start = day.x
@@ -712,7 +667,8 @@
                     event_item.width = column_width - 4
                     if columnno != (parallel - 1):
                         event_item.width += column_width / 1.2
-                    event_item.height = max(event_item.line_height, y_off2)
+                    event_item.height = max(
+                        event_item.get_line_height(self), y_off2)
                     if event.start < event1_start and event.end > event1_end:
                         event_item.type = 'mid'
                     elif event.start < event1_start:
@@ -721,7 +677,31 @@
                         event_item.type = 'bottom'
                     else:
                         event_item.type = 'topbottom'
-                    event_item.update()
+
+    def on_draw(self, widget, cr):
+        """Handle the draw signal - draw the entire calendar."""
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
+
+        # Draw background
+        cr.set_source_rgb(1, 1, 1)
+        cr.rectangle(0, 0, w, h)
+        cr.fill()
+
+        # Draw all visible day items
+        for day in self.days:
+            if day.visible:
+                day.draw(cr, self)
+
+        # Draw timeline for week/day views
+        if self.view in ("week", "day") and self._timeline.visible:
+            self._timeline.draw(cr, self)
+
+        # Draw all event items
+        for event_item in self._event_items:
+            event_item.draw(cr, self)
+
+        return False
 
     def on_event_store_event_removed(self, store, event):
         self.update()
@@ -743,8 +723,63 @@
         elif event.keyval == Gdk.KEY_Right:
             self.select(date + datetime.timedelta(1))
 
-    @util.left_click
-    def on_day_item_button_press_event(self, day, widget2, event):
+    def on_button_press_event(self, widget, event):
+        if event.button != 1:
+            return False
+
+        x, y = event.x, event.y
+
+        # Check if an event item was clicked
+        for event_item in reversed(self._event_items):
+            if event_item.contains_point(x, y):
+                self._handle_event_item_press(event_item, event)
+                return True
+
+        # Check if a day was clicked
+        for day in self.days:
+            if day.visible and day.contains_point(x, y):
+                self._handle_day_press(day, event)
+                return True
+
+        return False
+
+    def on_button_release_event(self, widget, event):
+        # Find if we're releasing on an event item
+        for event_item in self._event_items:
+            if event_item.contains_point(event.x, event.y):
+                if self._drag_x is not None:
+                    event_item.transparent = False
+                    self._stop_drag_and_drop()
+                    self.update()
+                    self.emit('event-released', event_item.event)
+                    return True
+        if self._drag_x is not None:
+            self._stop_drag_and_drop()
+            self.update()
+        return False
+
+    def on_motion_notify_event(self, widget, event):
+        if self._drag_x is None or self._drag_y is None:
+            return False
+
+        # Find the event item being dragged
+        for event_item in self._event_items:
+            if event_item.transparent:
+                self._handle_event_item_motion(event_item, event)
+                return True
+        return False
+
+    def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
+        # Check event items for tooltips
+        for event_item in reversed(self._event_items):
+            if event_item.contains_point(x, y):
+                tooltip_text = event_item.get_tooltip()
+                if tooltip_text:
+                    tooltip.set_text(tooltip_text)
+                    return True
+        return False
+
+    def _handle_day_press(self, day, event):
         self.emit('day-pressed', day.date)
         self.select(day.date)
 
@@ -776,7 +811,6 @@
         """
         if self.view == 'day':
             return self.selected_date
-        # Get current week
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         if self.view == 'week':
@@ -792,32 +826,28 @@
                 weekno = int(y / self._day_height)
             cur_week = weeks[weekno]
 
-        # Get Current pointed date
         max_width = 7 * self._day_width
         if x < 0:
             day_no = 0
         elif x > max_width:
             day_no = 6
         else:
-            offset_x = self._timeline.width if self.view == 'week' else 0
+            offset_x = (
+                self._timeline.get_width(self) if self.view == 'week' else 0)
             day_no = int((x - offset_x) / self._day_width)
         return cur_week[day_no]
 
-    @util.left_click
-    def on_event_item_button_press_event(self, event_item, rect, event):
-
+    def _handle_event_item_press(self, event_item, event):
         if event_item.event.editable:
-            # Drag and drop starting coordinates
             self._drag_x = event.x
             self._drag_y = event.y
             self._drag_height = 0
             self._drag_start_date = self.get_cur_pointed_date(event.x, event.y)
             self._drag_date = self._drag_start_date
             self.set_has_tooltip(False)
-            event_item.raise_(None)
             event_item.transparent = True
 
-            event_item.width = self._day_width - 6  # Biggest event width
+            event_item.width = self._day_width - 6
             event_date = event_item.event.start.date()
             daysdelta = self._drag_start_date - event_date
             if self.view == 'week':
@@ -829,24 +859,22 @@
                     if event_item.event.end:
                         event_item.event.end += daysdelta
                 else:
+                    # Remove other event items for this event
                     for item in event_item.event.event_items:
                         if item != event_item:
-                            item.remove()
                             self._event_items.remove(item)
 
                     event_item.height = 2 * self._line_height
-                    day_no = (int((event.x - self._timeline.width)
+                    day_no = (int((event.x - self._timeline.get_width(self))
                         / self._day_width))
                     day_off = day_no * self._day_width + 2
-                    event_item.x = self._timeline.width + day_off
+                    event_item.x = self._timeline.get_width(self) + day_off
                     if (event_item.no_caption or event.y < event_item.y
                             or event.y > (event_item.y + event_item.height)):
-                        # click was not performed inside the new day item
                         level_height = self.minute_height * self.MIN_PER_LEVEL
                         cur_level = int((event.y - self._timeline.y)
                             / level_height)
                         nb_levels_per_hour = 60 / self.MIN_PER_LEVEL
-                        # click is in the middle
                         cur_level -= nb_levels_per_hour
                         if cur_level < 0:
                             cur_level = 0
@@ -866,7 +894,6 @@
             elif self.view == 'month':
                 for item in event_item.event.event_items:
                     if item != event_item:
-                        item.remove()
                         self._event_items.remove(item)
                     else:
                         event_item.event.start += daysdelta
@@ -876,28 +903,23 @@
                         day_no = int(event.x / self._day_width)
                         event_item.y = weekno * self._day_height
                         event_item.y += (
-                            int(self._line_height) + 1)  # padding-top
+                            int(self._line_height) + 1)
                         event_item.x = (
-                            day_no * self._day_width + 2)  # padding-left
+                            day_no * self._day_width + 2)
                         item_height = (
-                            self._line_height + 2)  # 2px between items
+                            self._line_height + 2)
                         while event_item.y < event.y:
                             event_item.y += item_height
                         event_item.y -= item_height
                         event_item.no_caption = False
-            event_item.update()
+            self.queue_draw()
+
         self.emit('event-pressed', event_item.event)
 
         if self._is_double_click(event):
             self._stop_drag_and_drop()
             self.emit('event-activated', event_item.event)
 
-    def on_event_item_button_release(self, event_item, rect, event):
-        event_item.transparent = False
-        self._stop_drag_and_drop()
-        self.draw_events()
-        self.emit('event-released', event_item.event)
-
     def _stop_drag_and_drop(self):
         self._drag_x = None
         self._drag_y = None
@@ -906,77 +928,82 @@
         self._drag_date = None
         self.set_has_tooltip(True)
 
-    def on_event_item_motion_notified(self, event_item, rect, event):
-        if self._drag_x and self._drag_y:
-            # We are currently drag and dropping this event item
-            diff_y = event.y - self._drag_y
-            self._drag_x = event.x
-            self._drag_y = event.y
-            self._drag_height += diff_y
+    def _handle_event_item_motion(self, event_item, event):
+        diff_y = event.y - self._drag_y
+        self._drag_x = event.x
+        self._drag_y = event.y
+        self._drag_height += diff_y
 
-            cur_pointed_date = self.get_cur_pointed_date(event.x, event.y)
-            daysdelta = cur_pointed_date - self._drag_date
-            if self.view == 'month':
-                if cur_pointed_date != self._drag_date:
-                    event_item.event.start += daysdelta
-                    if event_item.event.end:
-                        event_item.event.end += daysdelta
-                    nb_lines = int(round(float(daysdelta.days) / 7))
-                    nb_columns = daysdelta.days - nb_lines * 7
-                    event_item.x += nb_columns * self._day_width
-                    self._drag_date = cur_pointed_date
-                event_item.y += diff_y
-                event_item.update()
-                return
-
-            # Handle horizontal translation
+        cur_pointed_date = self.get_cur_pointed_date(event.x, event.y)
+        daysdelta = cur_pointed_date - self._drag_date
+        if self.view == 'month':
             if cur_pointed_date != self._drag_date:
-                self._drag_date = cur_pointed_date
                 event_item.event.start += daysdelta
                 if event_item.event.end:
                     event_item.event.end += daysdelta
-                event_item.x += daysdelta.days * self._day_width
+                nb_lines = int(round(float(daysdelta.days) / 7))
+                nb_columns = daysdelta.days - nb_lines * 7
+                event_item.x += nb_columns * self._day_width
+                self._drag_date = cur_pointed_date
+            event_item.y += diff_y
+            self.queue_draw()
+            return
 
-            if event_item.event.multidays or event_item.event.all_day:
-                event_item.update()
-                return
+        # Handle horizontal translation
+        if cur_pointed_date != self._drag_date:
+            self._drag_date = cur_pointed_date
+            event_item.event.start += daysdelta
+            if event_item.event.end:
+                event_item.event.end += daysdelta
+            event_item.x += daysdelta.days * self._day_width
+
+        if event_item.event.multidays or event_item.event.all_day:
+            self.queue_draw()
+            return
 
-            # Compute vertical translation
-            diff_minutes = int(round(self._drag_height / self.minute_height))
-            diff_time = datetime.timedelta(minutes=diff_minutes)
-            old_start = event_item.event.start
-            new_start = old_start + diff_time
-            next_level = util.next_level(old_start, self.MIN_PER_LEVEL)
-            prev_level = util.prev_level(old_start, self.MIN_PER_LEVEL)
-            if diff_time >= datetime.timedelta(0) and new_start >= next_level:
-                new_start = util.prev_level(new_start, self.MIN_PER_LEVEL)
-            elif diff_time < datetime.timedelta(0) and new_start <= prev_level:
-                new_start = util.next_level(new_start, self.MIN_PER_LEVEL)
-            else:
-                # We stay at the same level
-                event_item.update()
-                return
+        # Compute vertical translation
+        diff_minutes = int(round(self._drag_height / self.minute_height))
+        diff_time = datetime.timedelta(minutes=diff_minutes)
+        old_start = event_item.event.start
+        new_start = old_start + diff_time
+        next_level = util.next_level(old_start, self.MIN_PER_LEVEL)
+        prev_level = util.prev_level(old_start, self.MIN_PER_LEVEL)
+        if diff_time >= datetime.timedelta(0) and new_start >= next_level:
+            new_start = util.prev_level(new_start, self.MIN_PER_LEVEL)
+        elif diff_time < datetime.timedelta(0) and new_start <= prev_level:
+            new_start = util.next_level(new_start, self.MIN_PER_LEVEL)
+        else:
+            self.queue_draw()
+            return
 
-            # Apply vertical translation
-            midnight = datetime.time()
-            old_start_midnight = datetime.datetime.combine(old_start, midnight)
-            onedaydelta = datetime.timedelta(days=1)
-            next_day_midnight = old_start_midnight + onedaydelta
-            if new_start.day < old_start.day:
-                new_start = old_start_midnight
-            elif new_start >= next_day_midnight:
-                seconds_per_level = 60 * self.MIN_PER_LEVEL
-                level_delta = datetime.timedelta(seconds=seconds_per_level)
-                last_level = next_day_midnight - level_delta
-                new_start = last_level
-            event_item.event.start = new_start
-            if event_item.event.end:
-                timedelta = new_start - old_start
-                event_item.event.end += timedelta
-            pxdelta = (timedelta.total_seconds() / 60 * self.minute_height)
-            event_item.y += pxdelta
-            event_item.update()
-            self._drag_height -= pxdelta
+        # Apply vertical translation
+        midnight = datetime.time()
+        old_start_midnight = datetime.datetime.combine(old_start, midnight)
+        onedaydelta = datetime.timedelta(days=1)
+        next_day_midnight = old_start_midnight + onedaydelta
+        if new_start.day < old_start.day:
+            new_start = old_start_midnight
+        elif new_start >= next_day_midnight:
+            seconds_per_level = 60 * self.MIN_PER_LEVEL
+            level_delta = datetime.timedelta(seconds=seconds_per_level)
+            last_level = next_day_midnight - level_delta
+            new_start = last_level
+        event_item.event.start = new_start
+        if event_item.event.end:
+            timedelta = new_start - old_start
+            event_item.event.end += timedelta
+        pxdelta = (timedelta.total_seconds() / 60 * self.minute_height)
+        event_item.y += pxdelta
+        self.queue_draw()
+        self._drag_height -= pxdelta
+
+    def get_root_item(self):
+        # Compatibility method for GooCanvas API
+        return self
+
+    def grab_focus(self, item=None):
+        # Compatibility method for GooCanvas API
+        super().grab_focus()
 
 
 GObject.signal_new('event-pressed',
@@ -1021,14 +1048,20 @@
     (GObject.TYPE_PYOBJECT,))
 
 
-class DayItem(GooCanvas.CanvasGroup):
+def parse_color(color_string):
+    """Parse a color string to RGB values (0-1 range)."""
+    color = Gdk.RGBA()
+    if color.parse(color_string):
+        return color.red, color.green, color.blue, color.alpha
+    return 0, 0, 0, 1
+
+
+class DayItem:
     """
-    A canvas item representing a day.
+    A data structure representing a day.
     """
 
     def __init__(self, cal, **kwargs):
-        super(DayItem, self).__init__()
-
         self._cal = cal
         self.x = kwargs.get('x', 0)
         self.y = kwargs.get('y', 0)
@@ -1044,41 +1077,75 @@
         self.n_lines = 0
         self.title_text_color = ""
         self.line_height = 0
+        self.visible = True
 
-        # Create canvas items.
-        self.border = GooCanvas.CanvasRect(parent=self)
-        self.text = GooCanvas.CanvasText(parent=self)
-        self.box = GooCanvas.CanvasRect(parent=self)
-        self.indic = GooCanvas.CanvasRect(parent=self)
+    def contains_point(self, px, py):
+        """Check if a point is within this day item."""
+        return (self.x <= px <= self.x + self.width
+            and self.y <= py <= self.y + self.height)
 
-    def update(self):
+    def compute_line_height(self, cal):
+        """Compute the line height based on font."""
         if not self.date:
             return
 
         week_day = self.date.weekday()
         day_name = calendar.day_name[week_day]
         caption = '%s %s' % (self.date.day, day_name)
-        self.text.set_property('font', self._cal.props.font)
-        self.text.set_property('text', caption)
-        logical_height = self.text.get_natural_extents()[1].height
-        line_height = int(math.ceil(float(logical_height) / Pango.SCALE))
+
+        # Create a Pango layout to measure text
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self.line_height = logical_rect.height
+
+        # Compute number of lines that fit
+        if self.full_border:
+            box_height = max(self.height - self.line_height - 3, 0)
+        else:
+            box_height = max(self.height - self.line_height, 0)
+        line_height_and_margin = self.line_height + 2
+        self.n_lines = (
+            int(box_height / line_height_and_margin)
+            if line_height_and_margin > 0
+            else 0)
+
+    def draw(self, cr, cal):
+        """Draw this day item using Cairo."""
+        if not self.date:
+            return
+
+        week_day = self.date.weekday()
+        day_name = calendar.day_name[week_day]
+        caption = '%s %s' % (self.date.day, day_name)
+
+        # Create a Pango layout
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        line_height = logical_rect.height
         self.line_height = line_height
 
-        # Draw the border.
-        self.border.set_property('x', self.x)
-        self.border.set_property('y', self.y)
-        self.border.set_property('width', self.width)
-        self.border.set_property('height', self.height)
-        self.border.set_property('stroke_color', self.border_color)
-        self.border.set_property('fill_color', self.border_color)
+        # Draw the border (filled rectangle)
+        r, g, b, a = parse_color(self.border_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.fill()
 
-        # Draw the title text.
+        # Draw the title text
+        r, g, b, a = parse_color(self.title_text_color)
+        cr.set_source_rgba(r, g, b, a)
         padding_left = 2
-        self.text.set_property('x', self.x + padding_left)
-        self.text.set_property('y', self.y)
-        self.text.set_property('fill_color', self.title_text_color)
+        cr.move_to(self.x + padding_left, self.y)
+        PangoCairo.show_layout(cr, layout)
 
-        # Print the "body" of the day.
+        # Draw the "body" of the day
         if self.full_border:
             box_x = self.x + 2
             box_y = self.y + line_height
@@ -1089,56 +1156,48 @@
             box_y = self.y + line_height
             box_width = max(self.width - 2, 0)
             box_height = max(self.height - line_height, 0)
-        self.box.set_property('x', box_x)
-        self.box.set_property('y', box_y)
-        self.box.set_property('width', box_width)
-        self.box.set_property('height', box_height)
-        self.box.set_property('stroke_color', self.body_color)
-        self.box.set_property('fill_color', self.body_color)
+
+        r, g, b, a = parse_color(self.body_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.rectangle(box_x, box_y, box_width, box_height)
+        cr.fill()
 
-        line_height_and_margin = line_height + 2  # 2px of margin per line
-        self.n_lines = int(box_height / line_height_and_margin)
-
-        # Show an indicator in the title, if requested.
-        if not self.show_indic:
-            self.indic.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
-            return
+        line_height_and_margin = line_height + 2
+        self.n_lines = (
+            int(box_height / line_height_and_margin)
+            if line_height_and_margin > 0
+            else 0)
 
-        self.indic.set_property(
-            'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-        self.indic.set_property('x',
-            self.x + self.width - line_height / 1.5)
-        self.indic.set_property('y', self.y + line_height / 3)
-        self.indic.set_property('width', line_height / 3)
-        self.indic.set_property('height', line_height / 3)
-        self.indic.set_property('stroke_color', self.title_text_color)
-        self.indic.set_property('fill_color', self.title_text_color)
-
-        # Draw a triangle.
-        x1 = self.x + self.width - line_height / 1.5
-        y1 = self.y + line_height / 3
-        x2 = x1 + line_height / 6
-        y2 = y1 + line_height / 3
-        x3 = x1 + line_height / 3
-        y3 = y1
-        path = 'M%s,%s L%s,%s L%s,%s Z' % (x1, y1, x2, y2, x3, y3)
-        self.indic.set_property('clip_path', path)
+        # Draw indicator if needed
+        if self.show_indic:
+            r, g, b, a = parse_color(self.title_text_color)
+            cr.set_source_rgba(r, g, b, a)
+            # Draw a triangle indicator
+            x1 = self.x + self.width - line_height / 1.5
+            y1 = self.y + line_height / 3
+            x2 = x1 + line_height / 6
+            y2 = y1 + line_height / 3
+            x3 = x1 + line_height / 3
+            y3 = y1
+            cr.move_to(x1, y1)
+            cr.line_to(x2, y2)
+            cr.line_to(x3, y3)
+            cr.close_path()
+            cr.fill()
 
 
-class EventItem(GooCanvas.CanvasGroup):
+class EventItem:
     """
-    A canvas item representing an event.
+    A data structure representing an event.
     """
 
     def __init__(self, cal, **kwargs):
-        super(EventItem, self).__init__()
-
         self._cal = cal
-        self.x = kwargs.get('x')
-        self.y = kwargs.get('y')
-        self.width = kwargs.get('width')
-        self.height = kwargs.get('height')
+        self.x = kwargs.get('x', 0)
+        self.y = kwargs.get('y', 0)
+        self.width = kwargs.get('width', 0)
+        self.height = kwargs.get('height', 0)
+        self.left_border = 0
         self.bg_color = kwargs.get('bg_color')
         self.text_color = kwargs.get('text_color', 'black')
         self.event = kwargs.get('event')
@@ -1146,239 +1205,273 @@
         self.time_format = kwargs.get('time_format')
         self.transparent = False
         self.no_caption = False
+        self._line_height = None
 
-        # Create canvas items.
-        self.box = GooCanvas.CanvasRect(parent=self)
-        self.text = GooCanvas.CanvasText(parent=self)
-        self.text.set_property('font', self._cal.props.font)
-        logical_height = self.text.get_natural_extents()[1].height
-        self.line_height = logical_height / Pango.SCALE
+    def get_line_height(self, cal):
+        """Get the line height for this event item."""
+        if self._line_height is not None:
+            return self._line_height
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text("Test", -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self._line_height = logical_rect.height
+        return self._line_height
+
+    def contains_point(self, px, py):
+        """Check if a point is within this event item."""
+        return (self.x <= px <= self.x + self.width
+            and self.y <= py <= self.y + self.height)
 
-        if self.x is not None:
-            self.update()
+    def get_tooltip(self):
+        """Get the tooltip text for this event."""
+        startdate = self.event.start.strftime('%x')
+        starttime = self.event.start.strftime(self.time_format)
+        if self.event.end:
+            enddate = self.event.end.strftime('%x')
+            endtime = self.event.end.strftime(self.time_format)
 
-    def update(self):
-        if (self.event.all_day or self._cal.view == "month"
+        if self.event.all_day:
+            if not self.event.end:
+                return '%s\n%s' % (startdate, self.event.caption)
+            else:
+                return '%s - %s\n%s' % (startdate, enddate, self.event.caption)
+        elif self.event.multidays:
+            if not self.event.end:
+                return '%s %s\n%s' % (startdate, starttime, self.event.caption)
+            else:
+                return '%s %s - %s %s\n%s' % (startdate, starttime,
+                    enddate, endtime, self.event.caption)
+        else:
+            if not self.event.end:
+                return '%s\n%s' % (starttime, self.event.caption)
+            else:
+                return '%s - %s\n%s' % (starttime, endtime, self.event.caption)
+
+    def draw(self, cr, cal):
+        """Draw this event item using Cairo."""
+        if (self.event.all_day or cal.view == "month"
                 or self.event.multidays):
-            self.update_all_day_event()
+            self._draw_all_day_event(cr, cal)
         else:
-            self.update_event()
+            self._draw_event(cr, cal)
 
-    def update_event(self):
+    def _draw_event(self, cr, cal):
         self.width = max(self.width, 0)
         starttime = self.event.start.strftime(self.time_format)
         endtime = self.event.end.strftime(self.time_format)
-        tooltip = '%s - %s\n%s' % (starttime, endtime, self.event.caption)
 
-        # Do we have enough width for caption
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
         first_line = starttime + ' - ' + endtime
-        self.text.set_property('text', first_line)
-        logical_width = self.text.get_natural_extents()[1].width / Pango.SCALE
-        if self.width < logical_width:
+        layout.set_text(first_line, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        line_height = logical_rect.height
+        self._line_height = line_height
+
+        if self.width < logical_rect.width:
             first_line = starttime + ' - '
 
         second_line = self.event.caption
-        self.text.set_property('text', second_line)
-        logical_width = self.text.get_natural_extents()[1].width / Pango.SCALE
-        if self.width < logical_width:
+        layout.set_text(second_line, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        if self.width < logical_rect.width:
             second_line = None
 
-        # Do we have enough height for whole caption
-        if self.height >= (2 * self.line_height):
+        if self.height >= (2 * line_height):
             caption = first_line
             if second_line:
                 caption += '\n' + second_line
-        elif self.height >= self.line_height:
+        elif self.height >= line_height:
             caption = first_line
         else:
             caption = ''
         caption = '' if self.no_caption else caption
         the_event_bg_color = self.event.bg_color
-
-        # Choose text color.
         if self.event.text_color is None:
             the_event_text_color = self.text_color
         else:
             the_event_text_color = self.event.text_color
 
         if the_event_bg_color is not None:
-            self.box.set_property('x', self.x)
-            self.box.set_property('y', self.y)
-            self.box.set_property('width', self.width)
-            self.box.set_property('height', self.height)
-            self.box.set_property('stroke_color', the_event_bg_color)
-            self.box.set_property('fill_color', the_event_bg_color)
-            # Alpha color is set to half of 255, i.e an opacity of 5O percents
-            transparent_color = self.box.get_property('fill_color_rgba') - 128
+            r, g, b, a = parse_color(the_event_bg_color)
             if self.transparent:
-                self.box.set_property('stroke_color_rgba', transparent_color)
-                self.box.set_property('fill_color_rgba', transparent_color)
-            self.box.set_property('tooltip', tooltip)
+                a = 0.5
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, self.y, self.width, self.height)
+            cr.fill()
 
-        # Print the event name into the title box.
-        self.text.set_property('x', self.x + 2)
-        self.text.set_property('y', self.y)
-        self.text.set_property('text', caption)
-        self.text.set_property('fill_color', the_event_text_color)
-        self.text.set_property('tooltip', tooltip)
+        # Draw the text
+        r, g, b, a = parse_color(the_event_text_color)
+        cr.set_source_rgba(r, g, b, a)
+
+        # Clip to event bounds
+        cr.save()
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.clip()
 
-        # Clip the text.
-        x2, y2 = self.x + self.width, self.y + self.height,
-        path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (self.x, self.y, self.x, y2,
-            x2, y2, x2, self.y)
-        self.text.set_property('clip_path', path)
+        layout.set_text(caption, -1)
+        cr.move_to(self.x + 2, self.y)
+        PangoCairo.show_layout(cr, layout)
 
-    def update_all_day_event(self):
+        cr.restore()
+
+    def _draw_all_day_event(self, cr, cal):
         self.width = max(self.width, 0)
-        startdate = self.event.start.strftime('%x')
         starttime = self.event.start.strftime(self.time_format)
-        if self.event.end:
-            enddate = self.event.end.strftime('%x')
-            endtime = self.event.end.strftime(self.time_format)
-        caption = self.event.caption
 
-        if self.event.all_day:
-            if not self.event.end:
-                tooltip = '%s\n%s' % (startdate, caption)
-            else:
-                tooltip = '%s - %s\n%s' % (startdate, enddate, caption)
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
+        caption = self.event.caption
+        if not self.event.all_day and not self.event.multidays:
+            caption = '%s %s' % (starttime, caption)
         elif self.event.multidays:
             caption = '%s %s' % (starttime, caption)
-            if not self.event.end:
-                tooltip = '%s %s\n%s' % (startdate, starttime, caption)
-            else:
-                tooltip = '%s %s - %s %s\n%s' % (startdate, starttime,
-                    enddate, endtime, caption)
-        else:
-            caption = '%s %s' % (starttime, caption)
-            if not self.event.end:
-                tooltip = '%s\n%s' % (starttime, caption)
-            else:
-                tooltip = '%s - %s\n%s' % (starttime, endtime, caption)
+
         caption = '' if self.no_caption else caption
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self.height = logical_rect.height
+        self._line_height = logical_rect.height
+
         the_event_bg_color = self.event.bg_color
-        self.text.set_property('text', caption)
-        logical_height = self.text.get_natural_extents()[1].height
-        self.height = logical_height / Pango.SCALE
-
-        # Choose text color.
         if self.event.text_color is None:
             the_event_text_color = self.text_color
         else:
             the_event_text_color = self.event.text_color
 
         if the_event_bg_color is not None:
-            self.box.set_property('x', self.x)
-            self.box.set_property('y', self.y)
-            self.box.set_property('width', self.width)
-            self.box.set_property('height', self.height)
-            self.box.set_property('stroke_color', the_event_bg_color)
-            self.box.set_property('fill_color', the_event_bg_color)
-            transparent_color = self.box.get_property('fill_color_rgba') - 128
+            r, g, b, a = parse_color(the_event_bg_color)
             if self.transparent:
-                self.box.set_property('stroke_color_rgba', transparent_color)
-                self.box.set_property('fill_color_rgba', transparent_color)
-            self.box.set_property('tooltip', tooltip)
+                a = 0.5
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, self.y, self.width, self.height)
+            cr.fill()
 
-        # Print the event name into the title box.
-        self.text.set_property('x', self.x + 2)
-        self.text.set_property('y', self.y)
-        self.text.set_property('fill_color', the_event_text_color)
-        self.text.set_property('tooltip', tooltip)
+        # Draw the text
+        r, g, b, a = parse_color(the_event_text_color)
+        cr.set_source_rgba(r, g, b, a)
 
-        # Clip the text.
-        x2, y2 = self.x + self.width, self.y + self.height,
-        path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (
-            self.x, self.y, self.x, y2, x2, y2, x2, self.y)
-        self.text.set_property('clip_path', path)
+        # Clip to event bounds
+        cr.save()
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.clip()
+
+        cr.move_to(self.x + 2, self.y)
+        PangoCairo.show_layout(cr, layout)
+
+        cr.restore()
 
 
-class TimelineItem(GooCanvas.CanvasGroup):
+class TimelineItem:
     """
-    A canvas item representing a timeline.
+    A data structure representing a timeline.
     """
 
     def __init__(self, cal, **kwargs):
-        super(TimelineItem, self).__init__()
-
         self._cal = cal
-        self.x = kwargs.get('x')
-        self.y = kwargs.get('y')
+        self.x = kwargs.get('x', 0)
+        self.y = kwargs.get('y', 0)
+        self.height = kwargs.get('height', 0)
         self.line_color = kwargs.get('line_color')
         self.bg_color = kwargs.get('bg_color')
         self.text_color = kwargs.get('text_color')
         self.time_format = kwargs.get('time_format')
-        self.width = 0
+        self._width = None
+        self._min_line_height = None
+        self.visible = False
+
+    def get_width(self, cal):
+        """Compute the width needed for the timeline."""
+        if self._width is not None:
+            return self._width
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
 
-        # Create canvas items.
-        self._timeline_rect = {}
-        self._timeline_text = {}
+        max_width = 0
+        for n in range(24):
+            caption = datetime.time(n).strftime(self.time_format)
+            layout.set_text(caption, -1)
+            ink_rect, logical_rect = layout.get_pixel_extents()
+            max_width = max(max_width, logical_rect.width)
+
+        self._width = max_width + 4  # Add some padding
+        return self._width
+
+    def get_min_line_height(self, cal):
+        """Get the minimum line height for the timeline."""
+        if self._min_line_height is not None:
+            return self._min_line_height
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
+        max_height = 0
         for n in range(24):
             caption = datetime.time(n).strftime(self.time_format)
-            self._timeline_rect[n] = GooCanvas.CanvasRect(parent=self)
-            self._timeline_text[n] = GooCanvas.CanvasText(
-                parent=self, text=caption)
+            layout.set_text(caption, -1)
+            _, logical_rect = layout.get_pixel_extents()
+            max_height = max(max_height, logical_rect.height)
 
-        if self.x is not None:
-            self.update()
-        else:
-            self._compute_width()
+        self._min_line_height = max_height
+        return self._min_line_height
 
-    @property
-    def min_line_height(self):
-        logical_height = 0
-        self.ink_padding_top = 0
-        for n in range(24):
-            natural_extents = self._timeline_text[n].get_natural_extents()
-            logical_rect = natural_extents[1]
-            logical_height = max(logical_height, logical_rect.height)
-            ink_rect = natural_extents[0]
-            self.ink_padding_top = max(self.ink_padding_top, ink_rect.x)
-        line_height = int(math.ceil(float(logical_height) / Pango.SCALE))
-        return line_height
+    def get_line_height(self, cal):
+        """Get the actual line height for drawing."""
+        min_height = self.get_min_line_height(cal)
+        if self.height > 0 and min_height < self.height // 24:
+            return self.height // 24
+        return min_height
 
-    @property
-    def line_height(self):
-        self.padding_top = 0
-        line_height = self.min_line_height
-        if line_height < self.height // 24:
-            line_height = self.height // 24
-            padding_top = (line_height - self._cal.font_size / Pango.SCALE) / 2
-            padding_top -= int(math.ceil(
-                    float(self.ink_padding_top) / Pango.SCALE))
-            self.padding_top = padding_top
-        return line_height
+    def draw(self, cr, cal):
+        """Draw the timeline using Cairo."""
+        width = self.get_width(cal)
+        line_height = self.get_line_height(cal)
+        min_line_height = self.get_min_line_height(cal)
+
+        # Compute padding for centering text
+        padding_top = 0
+        if line_height > min_line_height:
+            padding_top = (line_height - min_line_height) / 2
 
-    def _compute_width(self):
-        font = self._cal.props.font
-        ink_padding_left = 0
-        ink_max_width = 0
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
         for n in range(24):
-            self._timeline_text[n].set_property('font', font)
-            natural_extents = self._timeline_text[n].get_natural_extents()
-            ink_rect = natural_extents[0]
-            ink_padding_left = max(ink_padding_left, ink_rect.x)
-            ink_max_width = max(ink_max_width, ink_rect.width)
-        self.width = int(math.ceil(
-                float(ink_padding_left + ink_max_width) / Pango.SCALE))
-
-    def update(self):
-        self._compute_width()
-        line_height = self.line_height
+            y = self.y + n * line_height
+            caption = datetime.time(n).strftime(self.time_format)
 
-        # Draw the timeline.
-        for n in range(24):
-            rect = self._timeline_rect[n]
-            text = self._timeline_text[n]
-            y = self.y + n * line_height
+            # Draw background
+            r, g, b, a = parse_color(self.bg_color)
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, y, width, line_height)
+            cr.fill()
 
-            rect.set_property('x', self.x)
-            rect.set_property('y', y)
-            rect.set_property('width', self.width)
-            rect.set_property('height', line_height)
-            rect.set_property('stroke_color', self.line_color)
-            rect.set_property('fill_color', self.bg_color)
+            # Draw line separator
+            r, g, b, a = parse_color(self.line_color)
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, y, width, 1)
+            cr.fill()
 
-            text.set_property('x', self.x)
-            text.set_property('y', y + self.padding_top)
-            text.set_property('fill_color', self.text_color)
+            # Draw text
+            r, g, b, a = parse_color(self.text_color)
+            cr.set_source_rgba(r, g, b, a)
+            layout.set_text(caption, -1)
+            cr.move_to(self.x, y + padding_top)
+            PangoCairo.show_layout(cr, layout)
diff -r c9090b2733e3 -r 54b87bf46b5f pyproject.toml
--- a/pyproject.toml    Sun Mar 29 08:50:20 2026 +0200
+++ b/pyproject.toml    Mon May 04 11:12:23 2026 +0200
@@ -9,11 +9,11 @@
 maintainers = [
     {name = "Tryton", email = "[email protected]"},
     ]
-description = "A calendar widget for GTK using PyGoocanvas"
+description = "A calendar widget for GTK"
 readme = 'README.rst'
 license = 'GPL-2.0-or-later'
 license-files = ['LICENSE', 'COPYRIGHT']
-keywords = ["calendar", "GTK", "GooCanvas", "widget"]
+keywords = ["calendar", "GTK", "widget"]
 classifiers = [
     "Development Status :: 5 - Production/Stable",
     "Environment :: X11 Applications :: GTK",

Reply via email to