// The calendar that drops out of the clock — GNOME's date menu, minus the // notification list (that lives in its own panel). Month grid, today marked // with the accent, arrows to page through months, current weather at the foot. import Quickshell import QtQuick import qs.config import qs.services import qs.widgets Popover { id: root // Popover's container is a plain Item, which does not derive an implicit // size from its children, so the window has to be sized from the body. implicitWidth: body.implicitWidth + contentPadding * 2 implicitHeight: body.implicitHeight + contentPadding * 2 readonly property int cellSize: 34 readonly property int cellHeight: 30 // Hours precision is enough: the only thing that has to change on its own // is which cell counts as "today", and that only moves at midnight. SystemClock { id: clock precision: SystemClock.Hours } readonly property int todayYear: clock.date.getFullYear() readonly property int todayMonth: clock.date.getMonth() readonly property int todayDay: clock.date.getDate() // The month currently on screen. Reset to today every time the popover // opens, so it never comes back showing wherever you paged off to. property int viewYear: root.todayYear property int viewMonth: root.todayMonth onVisibleChanged: if (visible) root.showToday() function showToday(): void { root.viewYear = root.todayYear; root.viewMonth = root.todayMonth; } function stepMonth(delta: int): void { const d = new Date(root.viewYear, root.viewMonth + delta, 1); root.viewYear = d.getFullYear(); root.viewMonth = d.getMonth(); } // Six weeks of cells, so the grid height never changes as you page through // months. Days from the neighbouring months fill the edges, dimmed. readonly property var cells: { const first = new Date(root.viewYear, root.viewMonth, 1); const offset = first.getDay(); // 0 = Sunday, matching the header row const inThisMonth = new Date(root.viewYear, root.viewMonth + 1, 0).getDate(); const inPrevMonth = new Date(root.viewYear, root.viewMonth, 0).getDate(); const out = []; for (let i = 0; i < 42; i++) { const n = i - offset + 1; if (n < 1) out.push({ day: inPrevMonth + n, current: false }); else if (n > inThisMonth) out.push({ day: n - inThisMonth, current: false }); else out.push({ day: n, current: true }); } return out; } Column { id: body spacing: Theme.itemSpacing // ── Month header ──────────────────────────────────────────────────── Item { width: root.cellSize * 7 height: 28 CalendarArrow { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter glyph: "\u{F0141}" // md-chevron_left onActivated: root.stepMonth(-1) } Text { anchors.centerIn: parent text: Qt.formatDateTime(new Date(root.viewYear, root.viewMonth, 1), "MMMM yyyy") font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeLarge font.weight: Font.DemiBold color: Theme.fg MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.showToday() } } CalendarArrow { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter glyph: "\u{F0142}" // md-chevron_right onActivated: root.stepMonth(1) } } // ── Weekday header ────────────────────────────────────────────────── Row { Repeater { model: ["S", "M", "T", "W", "T", "F", "S"] delegate: Text { required property string modelData width: root.cellSize height: 20 text: modelData horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall color: Theme.fgMuted } } } // ── Day grid ──────────────────────────────────────────────────────── Grid { columns: 7 Repeater { model: root.cells delegate: Item { id: cell required property var modelData readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.viewMonth === root.todayMonth && root.viewYear === root.todayYear width: root.cellSize height: root.cellHeight Rectangle { anchors.centerIn: parent width: root.cellHeight - 2 height: root.cellHeight - 2 radius: width / 2 border.width: 0 visible: cell.isToday color: Theme.accent } Text { anchors.centerIn: parent text: cell.modelData.day font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: cell.isToday ? Font.DemiBold : Font.Normal color: { if (cell.isToday) return Theme.bgDark; return cell.modelData.current ? Theme.fg : Theme.fgMuted; } } } } } // ── Weather ───────────────────────────────────────────────────────── Rectangle { width: root.cellSize * 7 height: 1 color: Theme.alpha(Theme.fg, 0.1) visible: Weather.available } Row { spacing: Theme.itemSpacing visible: Weather.available Text { anchors.verticalCenter: parent.verticalCenter text: Weather.icon font.family: Theme.fontMono font.pixelSize: Theme.fontSizeTitle color: Theme.accentAlt } Column { Text { text: Math.round(Weather.temperature) + Weather.unitSuffix font.family: Theme.fontFamily font.pixelSize: Theme.fontSize color: Theme.fg } Text { text: Weather.description font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall color: Theme.fgDim } } } } }