Skip to content

Desktop Widget

Desktop widgets are floating, draggable components that appear on the desktop background. They provide at-a-glance information and can be positioned and scaled by the user in edit mode.

A desktop widget must extend DraggableDesktopWidget:

import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
DraggableDesktopWidget {
id: root
// Plugin API (injected by PluginService)
property var pluginApi: null
// Widget dimensions
implicitWidth: 200
implicitHeight: 120
// Your widget content here
}

Desktop widgets must import the qs.Modules.DesktopWidgets module to access the DraggableDesktopWidget base component:

import qs.Modules.DesktopWidgets

Injected by the PluginService. Provides access to plugin APIs and services.

property var pluginApi: null

The DraggableDesktopWidget base component provides these properties automatically:

The ShellScreen this widget is displayed on.

// Inherited from DraggableDesktopWidget
property ShellScreen screen

Configuration data for this widget instance (position, scale, custom settings).

// Access widget-specific data
property var widgetData: null
// Example usage
property color textColor: (widgetData && widgetData.textColor) ? widgetData.textColor : Color.mOnSurface

Index of this widget in the monitor’s widget list.

property int widgetIndex: -1

Whether the widget is currently being dragged (read-only).

readonly property bool isDragging

Whether the widget is currently being scaled (read-only).

readonly property bool isScaling

Whether to show the default background container. Can be overridden from widgetData.

property bool showBackground: true

Current scale factor of the widget. Controlled via right-click drag in edit mode.

property real widgetScale: 1.0
property real minScale: 0.5
property real maxScale: 3.0

Desktop widgets should define their natural size:

DraggableDesktopWidget {
id: root
// Fixed size
implicitWidth: 200
implicitHeight: 120
// Or dynamic size based on content
implicitWidth: contentLayout.implicitWidth + Style.marginXL * 2
implicitHeight: contentLayout.implicitHeight + Style.marginXL * 2
width: implicitWidth
height: implicitHeight
ColumnLayout {
id: contentLayout
// ...
}
}

Register the desktop widget in your plugin’s manifest.json:

{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"entryPoints": {
"desktopWidget": "DesktopWidget.qml"
}
}

Access plugin settings via pluginApi:

// Get setting with fallback to default
readonly property string message:
pluginApi?.pluginSettings?.message ||
pluginApi?.manifest?.metadata?.defaultSettings?.message ||
"Default Message"

Widget-specific configuration is available through widgetData. This is useful for per-instance settings:

DraggableDesktopWidget {
id: root
// Custom colors from widget data
property color textColor: {
var color = widgetData && widgetData.textColor ? widgetData.textColor : ""
return (color && color !== "") ? color : Color.mOnSurface
}
// Custom font size
property real fontSize: {
var size = widgetData && widgetData.fontSize ? widgetData.fontSize : 0
return (size && size > 0) ? size : Style.fontSizeL
}
// Opacity
property real widgetOpacity: (widgetData && widgetData.opacity) ? widgetData.opacity : 1.0
// Boolean options
property bool showDetails: (widgetData && widgetData.showDetails !== undefined) ? widgetData.showDetails : true
}

Use Noctalia’s design system for consistent styling:

import qs.Commons
NText {
// Primary text
color: Color.mOnSurface
// Secondary text
color: Color.mOnSurfaceVariant
// Accent color
color: Color.mPrimary
}
NText {
pointSize: Style.fontSizeS
pointSize: Style.fontSizeM
pointSize: Style.fontSizeL
pointSize: Style.fontSizeXL
pointSize: Style.fontSizeXXL
font.weight: Font.Light
font.weight: Font.Normal
font.weight: Font.Medium
font.weight: Font.Bold
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
spacing: Style.marginM
spacing: Style.marginL
}

The DraggableDesktopWidget provides a default styled background with shadow. You can control it this way but it’s preferred to keep as true if you don’t add your own custom background so users have the ability to disable/enable it via the settings:

DraggableDesktopWidget {
id: root
// Disable the default background (for transparent or custom backgrounds)
showBackground: false
// Or let it be controlled by widget settings
// showBackground is automatically bound to widgetData.showBackground if available
}

Users can position and scale desktop widgets in edit mode:

  • Left-click drag: Move the widget
  • Right-click drag: Scale the widget

Your widget should respond gracefully to scaling. The isDragging and isScaling properties let you adjust behavior during these operations:

DraggableDesktopWidget {
id: root
// Disable animations during drag/scale for better performance
Rectangle {
opacity: root.isDragging ? 0.8 : 1.0
Behavior on opacity {
enabled: !root.isDragging && !root.isScaling
NumberAnimation { duration: 200 }
}
}
}
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
DraggableDesktopWidget {
id: root
property var pluginApi: null
readonly property string message:
pluginApi?.pluginSettings?.message ||
pluginApi?.manifest?.metadata?.defaultSettings?.message ||
"Hello World"
// Scale dimensions by widgetScale
implicitWidth: Math.round(200 * widgetScale)
implicitHeight: Math.round(120 * widgetScale)
width: implicitWidth
height: implicitHeight
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.round(Style.marginL * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
NIcon {
icon: "noctalia"
pointSize: Math.round(Style.fontSizeXXL * widgetScale)
Layout.alignment: Qt.AlignHCenter
}
NText {
text: root.message
pointSize: Math.round(Style.fontSizeM * widgetScale)
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Desktop Widget"
pointSize: Math.round(Style.fontSizeS * widgetScale)
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
DraggableDesktopWidget {
id: root
property var pluginApi: null
// Customizable properties from widgetData
property color textColor: {
var color = widgetData && widgetData.textColor ? widgetData.textColor : ""
return (color && color !== "") ? color : Color.mOnSurface
}
property real baseFontSize: {
var size = widgetData && widgetData.fontSize ? widgetData.fontSize : 0
return (size && size > 0) ? size : Style.fontSizeL
}
property real widgetOpacity: (widgetData && widgetData.opacity) ? widgetData.opacity : 1.0
// Scale dimensions - use Math.round for pixel-perfect rendering
implicitWidth: Math.round((contentLayout.implicitWidth + Style.marginXL * 2) * widgetScale)
implicitHeight: Math.round((contentLayout.implicitHeight + Style.marginXL * 2) * widgetScale)
width: implicitWidth
height: implicitHeight
ColumnLayout {
id: contentLayout
anchors.centerIn: parent
spacing: Math.round(Style.marginM * widgetScale)
opacity: root.widgetOpacity
NText {
text: pluginApi?.pluginSettings?.message || "Custom Widget"
pointSize: Math.round(root.baseFontSize * widgetScale)
color: root.textColor
Layout.alignment: Qt.AlignHCenter
}
NText {
text: new Date().toLocaleDateString()
pointSize: Math.round(Style.fontSizeS * widgetScale)
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
DraggableDesktopWidget {
id: root
property var pluginApi: null
// Disable default background
showBackground: false
// Scale dimensions
implicitWidth: Math.round(300 * widgetScale)
implicitHeight: Math.round(150 * widgetScale)
width: implicitWidth
height: implicitHeight
// Custom transparent styling
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.round(Style.marginL * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
NText {
text: "12:34"
pointSize: Math.round(Style.fontSizeXXXL * 2 * widgetScale)
font.weight: Font.Light
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
// Optional: add shadow for visibility on any background
// Disable during scaling for performance
layer.enabled: !root.isScaling
layer.effect: MultiEffect {
shadowEnabled: true
shadowBlur: 0.5
shadowOpacity: 0.3
}
}
}
}
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
import qs.Services.Hardware
DraggableDesktopWidget {
id: root
property var pluginApi: null
// Scale dimensions
implicitWidth: Math.round(180 * widgetScale)
implicitHeight: Math.round(100 * widgetScale)
width: implicitWidth
height: implicitHeight
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.round(Style.marginL * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Math.round(Style.marginS * widgetScale)
NIcon {
icon: BatteryService.icon
pointSize: Math.round(Style.fontSizeL * widgetScale)
color: BatteryService.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(BatteryService.percentage) + "%"
pointSize: Math.round(Style.fontSizeL * widgetScale)
font.weight: Font.Bold
}
}
NText {
text: BatteryService.charging ? "Charging" : "On Battery"
pointSize: Math.round(Style.fontSizeS * widgetScale)
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}

Desktop widgets support user-controlled scaling from 0.5x to 5x. You must implement dimension-based scaling rather than using Qt’s transform-based scale property. Transform-based scaling usually cause jagged edges and overall poor rendering quality.

The base DraggableDesktopWidget provides a widgetScale property (default: 1.0) that represents the current scale factor. Your widget must multiply all dimension-related values by this property.

Multiply these values by widgetScale:

  1. Widget dimensions (implicitWidth, implicitHeight, width, height)
  2. Font sizes (pointSize, pixelSize)
  3. Margins and spacing (anchors.margins, Layout.margins, spacing)
  4. Border radii (radius)
  5. Icon sizes (pointSize for icons, baseSize for buttons)
  6. Fixed element sizes (image containers, buttons, etc.)

Always use Math.round() to ensure pixel-perfect values:

DraggableDesktopWidget {
id: root
// Scale widget dimensions
implicitWidth: Math.round(400 * widgetScale)
implicitHeight: Math.round(64 * widgetScale + Style.marginM * widgetScale * 2)
width: implicitWidth
height: implicitHeight
RowLayout {
anchors.fill: parent
// Scale margins and spacing
anchors.margins: Math.round(Style.marginM * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
// Scale fixed-size elements
Item {
Layout.preferredWidth: Math.round(48 * widgetScale)
Layout.preferredHeight: Math.round(48 * widgetScale)
NImageRounded {
anchors.fill: parent
// Scale border radius
radius: Math.round(Style.radiusM * widgetScale)
}
}
NText {
// Scale font sizes
pointSize: Math.round(Style.fontSizeM * widgetScale)
}
NIconButton {
// Scale button sizes
baseSize: Math.round(32 * widgetScale)
customRadius: Math.round(Style.radiusS * widgetScale)
}
}
}

Performance: Disabling Expensive Effects During Scaling

Section titled “Performance: Disabling Expensive Effects During Scaling”

The isScaling property is true while the user is actively resizing the widget. Use this to disable expensive operations that would cause lag:

DraggableDesktopWidget {
id: root
// Disable layer effects during scaling (expensive GPU operations)
Rectangle {
layer.enabled: Settings.data.general.enableShadows && !root.isScaling
layer.effect: MultiEffect {
shadowEnabled: true
shadowBlur: 0.5
}
}
// Disable drop shadows during scaling
NDropShadow {
visible: !root.isScaling
source: contentLayout
}
// Conditionally load expensive components
Loader {
active: shouldShowVisualizer && !root.isScaling
sourceComponent: ExpensiveVisualizerComponent {}
}
// Disable animations during scaling
Behavior on someProperty {
enabled: !root.isScaling && !root.isDragging
NumberAnimation { duration: 200 }
}
}
  • Layer effects (layer.enabled: false or layer.enabled: ... && !root.isScaling)
  • MultiEffect shadows (expensive GPU recomputations on every size change)
  • ShaderEffectSource (re-renders source item on every change)
  • Canvas/CustomPainter operations (expensive redraws)
  • Complex Loaders (set active: ... && !root.isScaling to completely unload)
  • Animations (disable Behaviors during scaling)
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Widgets
DraggableDesktopWidget {
id: root
property var pluginApi: null
// Scaled dimensions
implicitWidth: Math.round(300 * widgetScale)
implicitHeight: Math.round(100 * widgetScale)
width: implicitWidth
height: implicitHeight
// Drop shadow - disabled during scaling for performance
NDropShadow {
visible: !root.isScaling
anchors.fill: contentLayout
source: contentLayout
shadowBlur: 1.0
shadowOpacity: 0.9
}
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Math.round(Style.marginL * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
NText {
text: "Scaled Widget"
pointSize: Math.round(Style.fontSizeL * widgetScale)
font.weight: Font.Bold
Layout.alignment: Qt.AlignHCenter
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Math.round(Style.marginS * widgetScale)
NIconButton {
baseSize: Math.round(32 * widgetScale)
icon: "heart"
customRadius: Math.round(Style.radiusS * widgetScale)
}
NIconButton {
baseSize: Math.round(32 * widgetScale)
icon: "star"
customRadius: Math.round(Style.radiusS * widgetScale)
}
}
}
}
  1. Use DraggableDesktopWidget: Always extend this base component for proper drag/scale behavior
  2. Define implicit size: Set implicitWidth and implicitHeight for proper layout
  3. Implement dimension-based scaling: Multiply all sizes by widgetScale with Math.round()
  4. Disable expensive effects during scaling: Use isScaling to turn off layer effects, shadows, and complex components
  5. Handle widgetData: Use defensive checks when accessing widget-specific data
  6. Respect theming: Use Color.m* colors that adapt to light/dark mode
  7. Keep it performant: Avoid expensive operations since widgets are always visible
  8. Disable animations during interaction: Use isDragging and isScaling to optimize
  9. Provide fallbacks: Always have default values for settings and data

Users can add plugin desktop widgets through the Noctalia settings:

  1. Open Settings panel
  2. Navigate to Desktop Widgets
  3. Add new widget from the widget picker
  4. Enable edit mode
  5. Position and scale as desired
  6. Exit edit mode