Build the Panama Hyprland desktop

This commit is contained in:
Gabriel Brown
2026-08-17 10:32:55 -04:00
parent 67033f2a31
commit 5248883e4b
190 changed files with 18554 additions and 34 deletions
@@ -0,0 +1,233 @@
// The month grid from GNOME's date menu.
//
// Six week rows always, so the grid height never changes as you page through
// months and the panel below it never jumps. Days belonging to the neighbouring
// months fill the edges, dimmed. Today wears the accent.
//
// Deliberately non-interactive apart from the header: there is no calendar
// backend behind this, so a day cell that highlighted on hover would be
// promising a click that does nothing.
import Quickshell
import QtQuick
import qs.config
Item {
id: root
implicitHeight: body.implicitHeight
// Cells divide the available width evenly; the height is fixed so the grid
// stays roughly square regardless of how wide the panel gets.
readonly property real cellWidth: root.width / 7
readonly property int cellHeight: 32
// 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. DateMenu calls showToday() every time the
// panel opens, so it never comes back showing wherever you paged off to.
property int viewYear: root.todayYear
property int viewMonth: root.todayMonth
readonly property bool onCurrentMonth: root.viewYear === root.todayYear && root.viewMonth === root.todayMonth
function showToday(): void {
root.viewYear = root.todayYear;
root.viewMonth = root.todayMonth;
}
function stepMonth(delta: int): void {
// Built through Date rather than by hand so December -> January rolls
// the year over for us.
const d = new Date(root.viewYear, root.viewMonth + delta, 1);
root.viewYear = d.getFullYear();
root.viewMonth = d.getMonth();
}
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
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: 6
// ── Month header ────────────────────────────────────────────────────
// Both arrows sit on the right so the left slot is free for the "Today"
// affordance, which only exists while you are looking at another month.
Item {
width: parent.width
height: 30
Rectangle {
id: todayPill
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: todayLabel.implicitWidth + 18
height: 24
radius: Theme.pillRadius
border.width: 0
visible: !root.onCurrentMonth
color: todayMouse.containsMouse ? Theme.alpha(Theme.accent, 0.30) : Theme.alpha(Theme.accent, 0.16)
Behavior on color {
ColorAnimation {
duration: todayMouse.containsMouse ? Theme.durFast : Theme.durNormal
}
}
Text {
id: todayLabel
anchors.centerIn: parent
text: "Today"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: todayMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.showToday()
}
}
// Centred on the row rather than between the pill and the arrows,
// so it does not shift sideways when the pill appears.
Text {
anchors.centerIn: parent
text: Qt.formatDateTime(new Date(root.viewYear, root.viewMonth, 1), "MMMM yyyy")
font.family: Theme.fontFamily
// The year ticks in place when you page across a boundary.
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
color: Theme.fg
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
MonthArrow {
glyph: "\u{F0141}" // md-chevron_left
onActivated: root.stepMonth(-1)
}
MonthArrow {
glyph: "\u{F0142}" // md-chevron_right
onActivated: root.stepMonth(1)
}
}
}
// ── Weekday header ──────────────────────────────────────────────────
Row {
width: parent.width
Repeater {
model: ["S", "M", "T", "W", "T", "F", "S"]
delegate: Text {
required property string modelData
width: root.cellWidth
height: 22
text: modelData
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.fgMuted
}
}
}
// ── Day grid ────────────────────────────────────────────────────────
Grid {
width: parent.width
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.onCurrentMonth
width: root.cellWidth
height: root.cellHeight
Rectangle {
anchors.centerIn: parent
width: root.cellHeight - 4
height: root.cellHeight - 4
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.features: Theme.tabularFigures
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;
}
}
}
}
}
}
}
@@ -0,0 +1,111 @@
// GNOME's date/time menu: the calendar and the message tray in one panel,
// dropping from the top centre because that is where GNOME put it and the
// muscle memory is the whole point.
//
// A layer-shell surface rather than a PopupWindow because the inline reply
// fields have to be typeable, and because the clock it hangs from is owned by a
// different module.
//
// Layer-shell centres a surface on whichever axis it is not anchored to, so
// anchoring only `top` is what puts it in the middle.
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.config
import qs.services
import qs.widgets
PanelWindow {
id: root
// "notifications" is the existing overlay key, so Super+B, the notification
// IPC target and the clock all land here.
visible: ShellState.notificationsOpen
color: "transparent"
anchors.top: true
margins.top: Theme.barHeight + Theme.barGap * 2
exclusiveZone: 0
implicitWidth: panel.implicitWidth
implicitHeight: panel.implicitHeight
// The `qs-popover` prefix is matched by a layerrule in hypr/rules.lua.
WlrLayershell.namespace: "qs-popover-datemenu"
WlrLayershell.layer: WlrLayer.Overlay
// OnDemand rather than Exclusive: the panel must be able to take typing for
// inline replies, but must not steal it while merely visible.
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
onVisibleChanged: {
if (root.visible) {
// Opening the tray is what marks everything read, same as GNOME.
Notifs.markAllRead();
panel.prepare();
openAnim.restart();
} else {
panel.reset();
}
}
// Closes the panel when a click lands anywhere else on the desktop.
HyprlandFocusGrab {
windows: [root]
active: root.visible
onCleared: ShellState.close()
}
Rectangle {
id: surface
anchors.fill: parent
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.08)
// The prism edge — see widgets/PrismEdge.qml. Sits just inside the 1px
// border so the two don't fight for the same row of pixels.
PrismEdge {
anchors.top: parent.top
anchors.topMargin: 1
anchors.left: parent.left
anchors.right: parent.right
inset: parent.radius
}
// Grows out of the top edge it is anchored to, rather than out of its
// own centre — the panel should look like it came from the bar.
transform: Scale {
id: openScale
origin.x: surface.width / 2
origin.y: 0
xScale: 1
yScale: 1
}
// Escape reaches here by propagating up from whatever holds focus
// (usually a reply field), so it works in every sub-state.
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: ShellState.close()
DateMenuPanel {
id: panel
anchors.fill: parent
}
}
}
NumberAnimation {
id: openAnim
target: openScale
property: "yScale"
from: 0.94
to: 1.0
duration: Theme.durNormal
easing.type: Easing.OutCubic
}
}
@@ -0,0 +1,221 @@
// The contents of the date menu, in GNOME's order: date header, notifications,
// calendar, weather.
//
// Kept separate from DateMenu.qml (the PanelWindow) so it can be dropped into a
// FloatingWindow for testing — layer-shell surfaces cannot map outside a
// wlroots compositor.
import QtQuick
import Quickshell
import qs.config
import qs.services
import qs.widgets
Item {
id: root
implicitWidth: 480
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
// Called by the window when it opens and closes, so the panel is always in
// a known state rather than wherever the last visit left it.
function prepare(): void {
calendar.showToday();
}
function reset(): void {
notifications.reset();
}
// Minutes precision: the header only shows a date, so this exists purely to
// roll it over at midnight without a per-second wake.
SystemClock {
id: clock
precision: SystemClock.Minutes
}
Column {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.popoverPadding
spacing: Theme.sectionSpacing
// ── Header ──────────────────────────────────────────────────────────
Item {
width: parent.width
height: 34
Text {
anchors.left: parent.left
anchors.leftMargin: 4
anchors.right: headerActions.left
anchors.rightMargin: Theme.itemSpacing
anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDateTime(clock.date, "dddd, MMMM d")
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
// The day number changes in place at midnight.
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.DemiBold
}
Row {
id: headerActions
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
// Labelled rather than icon-only: an unexplained crossed-out
// bell is exactly the kind of thing a normal person has to
// click to find out about.
Rectangle {
id: dndPill
anchors.verticalCenter: parent.verticalCenter
width: dndRow.implicitWidth + 20
height: 28
radius: Theme.pillRadius
border.width: 0
color: {
if (Notifs.doNotDisturb)
return dndMouse.containsMouse ? Theme.alpha(Theme.accent, 0.42) : Theme.alpha(Theme.accent, 0.28);
return dndMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08);
}
Behavior on color {
ColorAnimation {
duration: dndMouse.containsMouse ? Theme.durFast : Theme.durNormal
}
}
Row {
id: dndRow
anchors.centerIn: parent
spacing: 6
ThemedIcon {
anchors.verticalCenter: parent.verticalCenter
size: 14
icon: "notifications-disabled-symbolic"
iconFallback: "user-invisible-symbolic"
tint: Notifs.doNotDisturb ? Theme.accent : Theme.fgDim
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Do Not Disturb"
color: Notifs.doNotDisturb ? Theme.accent : Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
}
MouseArea {
id: dndMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
}
}
Rectangle {
id: clearPill
anchors.verticalCenter: parent.verticalCenter
width: clearLabel.implicitWidth + 20
height: 28
radius: Theme.pillRadius
border.width: 0
// Hidden rather than disabled: there is nothing ambiguous
// about an empty tray, so the control has no job.
visible: Notifs.hasNotifications
color: clearMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08)
Behavior on color {
ColorAnimation {
duration: clearMouse.containsMouse ? Theme.durFast : Theme.durNormal
}
}
Text {
id: clearLabel
anchors.centerIn: parent
text: "Clear all"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: clearMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Notifs.dismissAll()
}
}
}
}
// ── Notifications ───────────────────────────────────────────────────
Rectangle {
width: parent.width
height: 1
border.width: 0
color: Theme.alpha(Theme.fg, 0.1)
}
MediaCard {
id: media
width: parent.width
}
Rectangle {
width: parent.width
height: 1
border.width: 0
visible: media.visible
color: Theme.alpha(Theme.fg, 0.1)
}
NotificationList {
id: notifications
width: parent.width
}
// ── Calendar ────────────────────────────────────────────────────────
Rectangle {
width: parent.width
height: 1
border.width: 0
color: Theme.alpha(Theme.fg, 0.1)
}
CalendarGrid {
id: calendar
width: parent.width
}
// ── Weather ─────────────────────────────────────────────────────────
// Separator and card share one condition so the panel does not end on a
// rule with nothing under it.
Rectangle {
width: parent.width
height: 1
border.width: 0
visible: Weather.available
color: Theme.alpha(Theme.fg, 0.1)
}
WeatherCard {
width: parent.width
visible: Weather.available
}
}
}
@@ -0,0 +1,150 @@
// Compact media controls in the date menu: artwork, useful metadata, and the
// three controls people expect. It disappears completely without a track.
import QtQuick
import Quickshell.Services.Mpris
import qs.config
import qs.widgets
Rectangle {
id: root
readonly property MprisPlayer player: {
const players = Mpris.players ? Mpris.players.values : [];
return players.find(p => p.isPlaying) ?? players.find(p => p.trackTitle) ?? null;
}
visible: root.player !== null
implicitHeight: visible ? 76 : 0
radius: Theme.cardRadius
border.width: 0
color: Theme.alpha(Theme.fg, 0.06)
clip: true
Rectangle {
id: artwork
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: 56
height: 56
radius: 10
border.width: 0
color: Theme.alpha(Theme.accentAlt, 0.16)
clip: true
Image {
anchors.fill: parent
source: root.player?.trackArtUrl ?? ""
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
visible: status === Image.Ready
}
ThemedIcon {
anchors.centerIn: parent
size: 24
icon: "audio-x-generic-symbolic"
iconFallback: "multimedia-player-symbolic"
tint: Theme.accentAlt
visible: !root.player?.trackArtUrl
}
}
Column {
anchors.left: artwork.right
anchors.leftMargin: 12
anchors.right: controls.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.player?.trackTitle || "Unknown track"
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: root.player?.trackArtist || root.player?.identity || "Media"
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Row {
id: controls
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 2
MediaButton {
icon: "media-skip-backward-symbolic"
enabled: root.player?.canGoPrevious ?? false
onClicked: root.player?.previous()
}
MediaButton {
icon: root.player?.isPlaying ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"
enabled: root.player?.canTogglePlaying ?? false
emphasized: true
onClicked: root.player?.togglePlaying()
}
MediaButton {
icon: "media-skip-forward-symbolic"
enabled: root.player?.canGoNext ?? false
onClicked: root.player?.next()
}
}
component MediaButton: Rectangle {
id: button
required property string icon
property bool emphasized: false
signal clicked
width: emphasized ? 34 : 30
height: width
radius: width / 2
border.width: 0
opacity: enabled ? 1 : 0.35
color: {
if (mouse.pressed)
return Theme.alpha(emphasized ? Theme.accent : Theme.fg, 0.34);
if (mouse.containsMouse)
return Theme.alpha(emphasized ? Theme.accent : Theme.fg, 0.22);
return emphasized ? Theme.alpha(Theme.accent, 0.14) : "transparent";
}
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
ThemedIcon {
anchors.centerIn: parent
size: 15
icon: button.icon
tint: button.emphasized ? Theme.accent : Theme.fg
}
MouseArea {
id: mouse
anchors.fill: parent
enabled: button.enabled
hoverEnabled: true
cursorShape: button.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: button.clicked()
}
}
}
@@ -0,0 +1,45 @@
// Month-stepping arrow in CalendarGrid's header. A round hover target sized
// like GNOME's calendar arrows rather than a bare glyph, so it reads as a
// button before the pointer is anywhere near it.
import QtQuick
import qs.config
Rectangle {
id: root
signal activated
required property string glyph
implicitWidth: 26
implicitHeight: 26
radius: width / 2
border.width: 0
color: mouse.containsMouse ? Theme.alpha(Theme.fg, Theme.hoverAlpha) : "transparent"
// Fast in, relaxed out — the arrow answers the pointer, so arriving should
// feel immediate and leaving should not snap.
Behavior on color {
ColorAnimation {
duration: mouse.containsMouse ? Theme.durFast : Theme.durNormal
}
}
Text {
anchors.centerIn: parent
text: root.glyph
// fontMono is the Nerd Font: used here to draw an icon, never text.
font.family: Theme.fontMono
font.pixelSize: Theme.fontSize
color: mouse.containsMouse ? Theme.fg : Theme.fgDim
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activated()
}
}
@@ -0,0 +1,172 @@
// One app's worth of notifications in the date menu, GNOME-style: a header
// carrying the app's identity and a clear button, over a collapsible stack of
// cards.
//
// The header is the disclosure control in its entirety — a 4px chevron is a
// poor click target, and GNOME lets you hit the whole row.
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.config
import qs.services
import qs.modules.notifications
import qs.modules.quicksettings
import qs.widgets
Column {
id: root
// One entry from Notifs.groups: { app, icon, desktopEntry, items }.
required property var group
property bool expanded: true
signal toggled
signal cleared
spacing: 4
// The app icon the notifications themselves declared, falling back to the
// desktop entry — same resolution order NotificationCard uses.
readonly property string iconSource: {
if (root.group.icon)
return Quickshell.iconPath(root.group.icon, true);
const entry = DesktopEntries.byId(root.group.desktopEntry);
return entry ? Quickshell.iconPath(entry.icon, true) : "";
}
Rectangle {
id: header
width: parent.width
height: 28
radius: Theme.cardRadius - 4
border.width: 0
color: headerMouse.containsMouse ? Theme.alpha(Theme.fg, 0.07) : "transparent"
Behavior on color {
ColorAnimation {
duration: headerMouse.containsMouse ? Theme.durFast : Theme.durNormal
}
}
MouseArea {
id: headerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled()
}
Row {
anchors.left: parent.left
anchors.leftMargin: 6
anchors.right: clearButton.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
spacing: 7
IconImage {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 15
asynchronous: true
source: root.iconSource
visible: source !== ""
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.group.app
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
// A bare count next to a collapsed group is the only way to know
// how much is hidden, so it is always shown once there is more
// than one.
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: countLabel.implicitWidth + 12
height: 16
radius: Theme.pillRadius
border.width: 0
visible: root.group.items.length > 1
color: Theme.alpha(Theme.fg, 0.12)
Text {
id: countLabel
anchors.centerIn: parent
text: root.group.items.length
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
}
IconButton {
id: clearButton
anchors.right: chevron.left
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
size: 22
iconSize: 12
tint: Theme.fgMuted
icon: "edit-clear-all-symbolic"
iconFallback: "window-close-symbolic"
onClicked: root.cleared()
}
ThemedIcon {
id: chevron
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
size: 12
icon: "pan-down-symbolic"
iconFallback: "go-down-symbolic"
tint: Theme.fgMuted
// Pointing down when open, right when closed — the standard
// disclosure direction, animated only on the click that caused it.
rotation: root.expanded ? 0 : -90
Behavior on rotation {
NumberAnimation {
duration: Theme.durNormal
easing.type: Easing.OutCubic
}
}
}
}
// Section animates its own height and hides itself at zero, which also
// collapses the spacing the parent Column would otherwise leave behind.
Section {
width: parent.width
expanded: root.expanded
Column {
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
Repeater {
model: root.group.items
NotificationCard {
required property var modelData
width: parent.width
compact: true
notification: modelData
onDismissed: Notifs.dismiss(modelData)
}
}
}
}
}
@@ -0,0 +1,73 @@
// The message tray inside the date menu: everything that has arrived, newest
// first, grouped by app, scrolling once it outgrows `maxHeight`.
import QtQuick
import qs.config
import qs.services
import qs.modules.quicksettings
Item {
id: root
property int maxHeight: 420
implicitHeight: Notifs.hasNotifications ? list.implicitHeight : empty.implicitHeight
// Which app groups are collapsed, keyed by app name. Kept here rather than
// on the group itself because Notifs.groups is rebuilt from scratch on
// every change to history, which destroys and recreates the delegates.
property var collapsedApps: ({})
function toggleApp(app: string): void {
// Reassigned rather than mutated: a property holding a JS object only
// re-evaluates bindings when the reference itself changes.
const next = Object.assign({}, root.collapsedApps);
if (next[app])
delete next[app];
else
next[app] = true;
root.collapsedApps = next;
}
// Called when the panel closes, so it always reopens fully expanded.
function reset(): void {
root.collapsedApps = ({});
}
Item {
id: empty
anchors.fill: parent
implicitHeight: 132
visible: !Notifs.hasNotifications
Text {
anchors.centerIn: parent
text: Notifs.doNotDisturb ? "Do Not Disturb is on" : "No notifications"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
}
ScrollColumn {
id: list
anchors.fill: parent
maxHeight: root.maxHeight
spacing: Theme.itemSpacing
visible: Notifs.hasNotifications
Repeater {
model: Notifs.groups
NotificationGroup {
required property var modelData
width: list.width
group: modelData
expanded: !root.collapsedApps[modelData.app]
onToggled: root.toggleApp(modelData.app)
onCleared: Notifs.dismissApp(modelData.app)
}
}
}
}
@@ -0,0 +1,65 @@
// Current conditions, at the foot of the date menu — GNOME's weather section.
//
// The whole card disappears when services/Weather.qml has nothing: a weather
// widget that cannot reach the network must go quiet, not show an error. The
// caller collapses the separator above it off the same `Weather.available`.
import QtQuick
import qs.config
import qs.services
Rectangle {
id: root
implicitHeight: 62
radius: Theme.cardRadius
// Cards inside an already-glass panel do not get a prism edge — the edge
// marks a pane of glass, not every item sitting on one.
border.width: 0
color: Theme.alpha(Theme.fg, 0.06)
Text {
id: glyph
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: Weather.icon
// fontMono is the Nerd Font, used here purely to draw an icon.
font.family: Theme.fontMono
font.pixelSize: 26
color: Theme.accentAlt
}
Column {
anchors.left: glyph.right
anchors.leftMargin: 14
anchors.right: parent.right
anchors.rightMargin: 16
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: Math.round(Weather.temperature) + Weather.unitSuffix
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
// The temperature changes in place on every refresh.
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
}
Text {
width: parent.width
// Description is empty for weather codes we don't have a label for,
// in which case the row is just the location.
text: Weather.description === "" ? Settings.weatherLocation : Weather.description + " · " + Settings.weatherLocation
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
}
@@ -0,0 +1,9 @@
module qs.modules.datemenu
CalendarGrid 1.0 CalendarGrid.qml
DateMenu 1.0 DateMenu.qml
DateMenuPanel 1.0 DateMenuPanel.qml
MediaCard 1.0 MediaCard.qml
MonthArrow 1.0 MonthArrow.qml
NotificationGroup 1.0 NotificationGroup.qml
NotificationList 1.0 NotificationList.qml
WeatherCard 1.0 WeatherCard.qml