settings: rebuild wallpaper page around a single hero thumbnail

cc/edit: fix janky resize problems
This commit is contained in:
bbedward
2026-09-20 20:42:16 -04:00
parent 9b6c9a4b36
commit d9d9e6b9cf
18 changed files with 394 additions and 399 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1789924572,
"narHash": "sha256-0zO+HFepbuUBedfE+FRoQ9XVFqsK9sXcqBU6NoAnlPQ=",
"lastModified": 1789949743,
"narHash": "sha256-nGF/lvqxo4GL/H+g+nVy/OcfkVmDIHpXs+HJqPiNvO4=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "08f2850f457c36585700fea489f3403389537faf",
"rev": "bb889a06932e20116f9d2d57d0f6c04f94fffcfe",
"type": "github"
},
"original": {
+15
View File
@@ -212,6 +212,21 @@ function crossGap(a, aSize, b, bSize) {
return Math.max(0, b - (a + aSize), a - (b + bSize));
}
function nearestStep(edgeFor, from, minStep, maxStep, wanted) {
const distance = step => Math.abs(edgeFor(step) - wanted);
let best = Math.max(minStep, Math.min(maxStep, from));
for (const direction of [1, -1]) {
for (let step = best + direction; step >= minStep && step <= maxStep; step += direction) {
const gap = distance(step);
if (gap > distance(best))
break;
if (gap < distance(best))
best = step;
}
}
return best;
}
function dimension(value, minimum, maximum, fallback, step = 1) {
const min = minimum ?? 1;
const max = Math.max(min, maximum ?? fallback ?? min);
@@ -15,6 +15,7 @@ DankModal {
property bool saveMode: false
property bool folderMode: false
property string defaultFileName: ""
property string revealPath: ""
property var parentPopout: null
signal fileSelected(string path)
@@ -59,6 +60,7 @@ DankModal {
saveMode: fileBrowserSurfaceModal.saveMode
folderMode: fileBrowserSurfaceModal.folderMode
defaultFileName: fileBrowserSurfaceModal.defaultFileName
revealPath: fileBrowserSurfaceModal.revealPath
Component.onCompleted: initialize()
@@ -17,6 +17,7 @@ Item {
signal powerButtonClicked
signal lockRequested
signal editModeToggled
signal editCancelled
signal settingsButtonClicked
signal headerTapped
@@ -93,7 +94,6 @@ Item {
Row {
id: actionButtonsRow
width: CcMetrics.headerActionSize * 4 + spacing * 3
height: CcMetrics.headerActionSize
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -126,6 +126,16 @@ Item {
onClicked: root.settingsButtonClicked()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
iconName: "close"
iconColor: Theme.surfaceText
visible: root.editMode
Accessible.name: I18n.tr("Cancel")
onClicked: root.editCancelled()
}
DankActionButton {
buttonSize: CcMetrics.headerActionSize
iconSize: CcMetrics.headerActionIconSize
@@ -26,6 +26,7 @@ FocusScope {
return total;
}
property var pageHistory: []
property var editSnapshot: null
readonly property bool panelResizing: panelResizer.resizing
readonly property real sheetContentWidth: host.sheetContentWidth ?? CcMetrics.sheetWidthFor(gridColumns)
readonly property vector4d surfaceCornerRadii: host.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius)
@@ -125,12 +126,28 @@ FocusScope {
configOverlayLoader.active = false;
}
function cancelEdit() {
const snapshot = editSnapshot;
host.editMode = false;
if (!snapshot)
return;
if (JSON.stringify(SettingsData.controlCenterWidgets) !== snapshot.widgets)
SettingsData.set("controlCenterWidgets", JSON.parse(snapshot.widgets));
if (SettingsData.controlCenterColumns !== snapshot.columns)
SettingsData.set("controlCenterColumns", snapshot.columns);
}
Keys.onEscapePressed: event => {
if (configOverlayLoader.item?.visible) {
configOverlayLoader.item.close();
event.accepted = true;
return;
}
if (host.editMode) {
host.editMode = false;
event.accepted = true;
return;
}
if (pageOpen) {
goBack();
event.accepted = true;
@@ -151,10 +168,16 @@ FocusScope {
}
onEditModeChanged: {
if (editMode)
if (editMode) {
host.collapseAll();
else
editSnapshot = {
"widgets": JSON.stringify(SettingsData.controlCenterWidgets),
"columns": SettingsData.controlCenterColumns
};
} else {
panelResizer.cancel();
editSnapshot = null;
}
forceActiveFocus();
}
@@ -165,7 +188,8 @@ FocusScope {
anchors.margins: -(contentInset + Theme.spacingS)
z: 1
visible: root.host.editMode
edgeResize: true
edgeResize: root.panelResizing || root.panelResizer.sideMovable(-1, root.gridColumns)
cornerResize: root.panelResizing || root.panelResizer.sideMovable(1, root.gridColumns)
horizontalResize: true
removable: false
cornerRadius: Theme.windowRadius + Theme.spacingS
@@ -233,6 +257,7 @@ FocusScope {
tapToClose: root.host.headerTogglesClose ?? false
onHeaderTapped: root.host.close()
onEditModeToggled: root.host.editMode = !root.host.editMode
onEditCancelled: root.cancelEdit()
onPowerButtonClicked: {
const loader = root.host.powerMenuModalLoader;
if (!loader)
@@ -15,6 +15,7 @@ DankPopout {
resizing: contentLoader.item?.panelResizing ?? false
surfacePadding: PopoutMetrics.editOverflow * 2
inputMargin: editMode ? PopoutMetrics.editOverflow * 2 : 0
minimumSurfaceWidth: editMode ? CcMetrics.sheetWidthFor(gridColumnCap) : 0
property string expandedSection: ""
property string pendingSection: ""
@@ -82,7 +82,7 @@ DankPopout {
readonly property real editGutter: editMode ? PopoutMetrics.editOverflow : 0
popupWidth: panelWidthFor(DashMetrics.panelColumnsFor(activeTabId))
minimumSurfaceWidth: panelWidthFor(DashRegistry.widestPanelColumns)
minimumSurfaceWidth: panelWidthFor(editMode ? columnCap : DashRegistry.widestPanelColumns)
popupHeight: contentLoader.item?.implicitHeight ?? (DashMetrics.tabDefaultHeight + Theme.navigationHeight + DashMetrics.contentGap + DashMetrics.contentPadding * 2)
triggerWidth: DashMetrics.triggerWidth
screen: triggerScreen
@@ -571,8 +571,8 @@ DankPopout {
anchors.margins: -(contentInset + Theme.spacingS)
z: 2
visible: root.editMode
edgeResize: mainContainer.panelResizer.sideMovable(-1, mainContainer.panelColumns)
cornerResize: mainContainer.panelResizer.sideMovable(1, mainContainer.panelColumns)
edgeResize: mainContainer.panelResizing || mainContainer.panelResizer.sideMovable(-1, mainContainer.panelColumns)
cornerResize: mainContainer.panelResizing || mainContainer.panelResizer.sideMovable(1, mainContainer.panelColumns)
removable: false
cornerRadius: Theme.windowRadius + Theme.spacingS
handleOverhang: contentInset
@@ -515,6 +515,7 @@ Item {
browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
browserType: "wallpaper"
showHiddenFiles: false
revealPath: root.getCurrentWallpaper()
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
parentPopout: root.parentPopout
@@ -150,6 +150,7 @@ Item {
browserTitle: I18n.tr("Select lock screen background image")
browserType: "wallpaper"
showHiddenFiles: true
revealPath: SettingsData.lockScreenWallpaperPath || Theme.wallpaperPath
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.svg"]
onFileSelected: path => {
SettingsData.set("lockScreenWallpaperPath", path);
@@ -21,7 +21,6 @@ Item {
readonly property string matugenPreviewSource: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary).toString()
readonly property string matugenPreviewImage: (!SettingsData.matugenSeedColor && Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : ""
readonly property string matugenPreviewKey: matugenPreviewSource + "|" + (SettingsData.matugenContrast ?? 0) + "|" + matugenPreviewImage + "|" + SettingsData.matugenSpec
property bool matugenGenerating: false
readonly property var currentPalette: ThemePalette.pick({
"primary": Theme.primary,
"secondary": Theme.secondary,
@@ -32,7 +31,7 @@ Item {
"warning": Theme.warning
})
property bool matugenPreviewFailed: false
readonly property bool matugenPreviewsReady: !matugenGenerating && (matugenPreviewLoadedKey === matugenPreviewKey || matugenPreviewFailed || !Theme.matugenAvailable)
readonly property bool matugenPreviewsReady: matugenPreviewLoadedKey === matugenPreviewKey || matugenPreviewFailed || !Theme.matugenAvailable
onMatugenPreviewKeyChanged: refreshMatugenSchemePreviews()
property var installedRegistryThemes: []
readonly property var matugenSchemeOptions: {
@@ -114,12 +113,6 @@ Item {
function onMatugenAvailableChanged() {
themeColorsTab.refreshMatugenSchemePreviews();
}
function onThemeGenerationStarting() {
themeColorsTab.matugenGenerating = true;
}
function onMatugenCompleted() {
themeColorsTab.matugenGenerating = false;
}
}
SettingsPage {
+154 -201
View File
@@ -14,7 +14,6 @@ Column {
id: root
property var parentModal: null
property string browseTarget: "desktop"
property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes
@@ -27,7 +26,6 @@ Column {
}
readonly property bool hasWallpaper: currentWallpaper !== ""
readonly property bool wallpaperIsImage: hasWallpaper && !currentWallpaper.startsWith("#")
readonly property bool canCycleWallpaper: wallpaperIsImage && !currentWallpaper.startsWith("we")
readonly property bool cyclingEnabled: {
SessionData.monitorCyclingSettings;
return perMonitor ? SessionData.getMonitorCyclingSettings(selectedScreen).enabled : SessionData.wallpaperCyclingEnabled;
@@ -41,6 +39,7 @@ Column {
"error": Theme.error,
"warning": Theme.warning
})
readonly property string modeLabel: SessionData.isLightMode ? I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode") : I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode")
readonly property string colorModeStatus: {
if (SettingsData.matugenSmartMode)
return I18n.tr("Wallpaper");
@@ -92,22 +91,13 @@ Column {
return I18n.tr("No displays");
}
function openBrowser(target) {
browseTarget = target;
function openBrowser() {
wallpaperBrowserLoader.active = true;
if (wallpaperBrowserLoader.item)
wallpaperBrowserLoader.item.open();
}
function applyBrowsedPath(path) {
switch (browseTarget) {
case "light":
SessionData.setWallpaperForMode(path, true);
return;
case "dark":
SessionData.setWallpaperForMode(path, false);
return;
}
function applyWallpaper(path) {
if (perMonitor) {
SessionData.setMonitorWallpaper(selectedScreen, path);
SessionData.setMonitorCyclingFolderPath(selectedScreen, "");
@@ -118,25 +108,28 @@ Column {
SessionData.saveSettings();
}
function pickColor(target, current, title) {
function pickColor() {
const picker = PopoutService.colorPickerModal;
if (!picker)
return;
picker.selectedColor = current.startsWith("#") ? current : Theme.primary;
picker.pickerTitle = title;
picker.selectedColor = currentWallpaper.startsWith("#") ? currentWallpaper : Theme.primary;
picker.pickerTitle = I18n.tr("Choose Wallpaper Color", "wallpaper color picker title");
picker.onColorSelectedCallback = function (color) {
root.browseTarget = target;
root.applyBrowsedPath(color.toString());
root.applyWallpaper(color.toString());
};
picker.show();
}
function clearDesktopWallpaper() {
function clearWallpaper() {
if (perMonitor) {
SessionData.setMonitorWallpaper(selectedScreen, "");
SessionData.setMonitorCyclingFolderPath(selectedScreen, "");
return;
}
if (perMode) {
SessionData.setWallpaperForMode("", SessionData.isLightMode);
return;
}
if (Theme.currentTheme === Theme.dynamic)
Theme.switchTheme("blue");
SessionData.clearWallpaper();
@@ -144,24 +137,6 @@ Column {
SessionData.saveSettings();
}
function cycleWallpaper(previous) {
if (!canCycleWallpaper)
return;
if (perMonitor) {
if (previous) {
WallpaperCyclingService.cyclePrevForMonitor(selectedScreen);
return;
}
WallpaperCyclingService.cycleNextForMonitor(selectedScreen);
return;
}
if (previous) {
WallpaperCyclingService.cyclePrevManually();
return;
}
WallpaperCyclingService.cycleNextManually();
}
ConfigInclude {
id: cursorInclude
includeKind: "cursor"
@@ -189,10 +164,87 @@ Column {
ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set one of:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct OR\nQT_QPA_PLATFORMTHEME=qtengine\nas environment variables, and then restart the shell.\n\nOnly qt6ct requires qt6ct-kde to be installed.", "qt theme env error body"));
}
component QuickTile: Rectangle {
id: tile
property string title: ""
property string hint: ""
default property alias leading: leadingSlot.data
signal clicked
width: parent.width
height: Theme.listItemTwoLineHeight
radius: Theme.cornerRadiusM
color: Theme.foregroundColor(Theme.chipSurface, true)
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: title
Accessible.description: hint
Accessible.onPressAction: tile.clicked()
Keys.onSpacePressed: tile.clicked()
Keys.onReturnPressed: tile.clicked()
FocusRing {}
Item {
id: leadingSlot
width: Theme.avatarSize
height: Theme.avatarSize
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.left: leadingSlot.right
anchors.leftMargin: Theme.spacingM
anchors.right: chevron.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
width: parent.width
text: tile.title
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: tile.hint
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankIcon {
id: chevron
name: "chevron_right"
size: Theme.iconSize
color: Theme.surfaceVariantText
rotation: I18n.isRtl ? 180 : 0
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
StateLayer {
stateColor: Theme.surfaceText
cornerRadius: tile.radius
onClicked: tile.clicked()
}
}
SettingsCard {
tab: "wallpaper"
tags: ["background", "image", "picture", "light", "dark", "mode", "monitor", "display"]
title: I18n.tr("Wallpaper")
tags: ["background", "image", "picture", "light", "dark", "mode", "theme", "color", "palette"]
title: I18n.tr("Appearance")
settingKey: "wallpaper"
SettingsDropdownRow {
@@ -210,189 +262,89 @@ Column {
settingKey: "colorMode"
tags: ["light", "dark", "mode", "theme", "color", "palette"]
body: Flow {
id: thumbs
id: hero
readonly property real thumbWidth: (width - spacing) / 2
readonly property bool stacked: width < SettingsMetrics.wallpaperHeroStackWidth
readonly property real thumbWidth: stacked ? width : Math.round(width * SettingsMetrics.wallpaperHeroSplit)
width: parent.width
spacing: Theme.spacingL
SettingsWallpaperThumb {
width: thumbs.thumbWidth
visible: !root.perMode
width: hero.thumbWidth
path: root.currentWallpaper
canCycle: root.canCycleWallpaper
onBrowse: root.openBrowser("desktop")
onPickColor: root.pickColor("desktop", root.currentWallpaper, I18n.tr("Choose Wallpaper Color", "wallpaper color picker title"))
onClear: root.clearDesktopWallpaper()
onPrevious: root.cycleWallpaper(true)
onNext: root.cycleWallpaper(false)
badge: root.perMode ? root.modeLabel : ""
onBrowse: root.openBrowser()
onPickColor: root.pickColor()
onClear: root.clearWallpaper()
}
SettingsWallpaperThumb {
width: thumbs.thumbWidth
visible: root.perMode
label: I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode")
placeholderIcon: "light_mode"
path: SessionData.wallpaperPathLight
canCycle: SessionData.isLightMode && root.canCycleWallpaper
onBrowse: root.openBrowser("light")
onPickColor: root.pickColor("light", SessionData.wallpaperPathLight, I18n.tr("Choose Light Mode Color", "light mode wallpaper color picker title"))
onClear: {
root.browseTarget = "light";
root.applyBrowsedPath("");
}
onPrevious: root.cycleWallpaper(true)
onNext: root.cycleWallpaper(false)
}
Column {
width: hero.stacked ? hero.width : hero.width - hero.thumbWidth - hero.spacing
spacing: Theme.spacingM
SettingsWallpaperThumb {
width: thumbs.thumbWidth
visible: root.perMode
label: I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode")
placeholderIcon: "dark_mode"
path: SessionData.wallpaperPathDark
canCycle: !SessionData.isLightMode && root.canCycleWallpaper
onBrowse: root.openBrowser("dark")
onPickColor: root.pickColor("dark", SessionData.wallpaperPathDark, I18n.tr("Choose Dark Mode Color", "dark mode wallpaper color picker title"))
onClear: {
root.browseTarget = "dark";
root.applyBrowsedPath("");
}
onPrevious: root.cycleWallpaper(true)
onNext: root.cycleWallpaper(false)
}
Item {
id: quickPanel
width: root.perMode ? thumbs.width : thumbs.thumbWidth
height: root.perMode ? quickColumn.implicitHeight : thumbs.thumbWidth * SettingsMetrics.wallpaperThumbRatio
Column {
id: quickColumn
DankButtonGroup {
width: parent.width
spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Row {
id: modeRow
width: parent.width
spacing: Theme.spacingS
DankButtonGroup {
width: parent.width - scheduleButton.width - parent.spacing
fillWidth: true
checkEnabled: false
anchors.verticalCenter: parent.verticalCenter
model: [
{
"text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"),
"icon": "light_mode"
},
{
"text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"),
"icon": "dark_mode"
}
]
currentIndex: SessionData.isLightMode ? 0 : 1
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const light = index === 0;
if (light === SessionData.isLightMode)
return;
Theme.screenTransition();
Theme.setLightMode(light);
}
}
DankActionButton {
id: scheduleButton
iconName: "schedule"
anchors.verticalCenter: parent.verticalCenter
Accessible.name: I18n.tr("Dark mode")
tooltipText: root.colorModeStatus
onClicked: root.parentModal?.navigateTo("theme_schedule")
fillWidth: true
checkEnabled: false
model: [
{
"text": I18n.tr("Light", "adjective, wallpaper thumbnail label for light mode"),
"icon": "light_mode"
},
{
"text": I18n.tr("Dark", "adjective, wallpaper thumbnail label for dark mode"),
"icon": "dark_mode"
}
]
currentIndex: SessionData.isLightMode ? 0 : 1
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
const light = index === 0;
if (light === SessionData.isLightMode)
return;
Theme.screenTransition();
Theme.setLightMode(light);
}
}
Rectangle {
id: themeTile
width: parent.width
height: Theme.listItemTwoLineHeight
radius: Theme.cornerRadiusM
color: Theme.foregroundColor(Theme.chipSurface, true)
QuickTile {
title: I18n.tr("Dark mode")
hint: root.colorModeStatus
onClicked: root.parentModal?.navigateTo("theme_schedule")
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: I18n.tr("Theme & colors")
Accessible.description: Theme.currentThemeLabel
Accessible.onPressAction: root.parentModal?.navigateTo("theme")
Keys.onSpacePressed: root.parentModal?.navigateTo("theme")
Keys.onReturnPressed: root.parentModal?.navigateTo("theme")
DankIcon {
anchors.centerIn: parent
name: "schedule"
size: Theme.iconSize
color: Theme.primary
}
}
FocusRing {}
QuickTile {
title: I18n.tr("Theme & colors")
hint: Theme.currentThemeLabel
onClicked: root.parentModal?.navigateTo("theme")
DankPaletteSwatch {
id: themeSwatch
width: Theme.avatarSize
height: Theme.avatarSize
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
primaryColor: root.themePalette.primary
secondaryColor: root.themePalette.secondary
tertiaryColor: root.themePalette.tertiary
}
Column {
anchors.left: themeSwatch.right
anchors.leftMargin: Theme.spacingM
anchors.right: themeChevron.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
StyledText {
width: parent.width
text: I18n.tr("Theme & colors")
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: Theme.currentThemeLabel
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankIcon {
id: themeChevron
name: "chevron_right"
size: Theme.iconSize
color: Theme.surfaceVariantText
rotation: I18n.isRtl ? 180 : 0
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
StateLayer {
stateColor: Theme.surfaceText
cornerRadius: themeTile.radius
onClicked: root.parentModal?.navigateTo("theme")
}
DankPaletteSwatch {
anchors.fill: parent
primaryColor: root.themePalette.primary
secondaryColor: root.themePalette.secondary
tertiaryColor: root.themePalette.tertiary
}
}
}
}
}
}
SettingsCard {
tab: "wallpaper"
tags: ["background", "image", "picture", "fill", "cycling", "transition", "monitor", "display"]
title: I18n.tr("Wallpaper")
settingKey: "wallpaperOptions"
SettingsDropdownRow {
id: fillModeRow
@@ -860,9 +812,10 @@ Column {
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserType: "wallpaper"
showHiddenFiles: true
revealPath: root.currentWallpaper
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
onFileSelected: path => {
root.applyBrowsedPath(path);
root.applyWallpaper(path);
close();
}
}
@@ -7,8 +7,8 @@ import qs.Common
Singleton {
readonly property real sidebarWidth: 320
readonly property real compactBreakpoint: 700
readonly property real contentMaxWidth: 640
readonly property real windowWidth: 1000
readonly property real contentMaxWidth: 720
readonly property real windowWidth: 1100
readonly property real windowHeight: 940
readonly property real windowMinWidth: 500
readonly property real windowMinHeight: 400
@@ -29,6 +29,8 @@ Singleton {
readonly property real buttonGroupCompactThreshold: 200
readonly property real choiceCardPreviewRatio: 10 / 16
readonly property real wallpaperThumbRatio: 10 / 16
readonly property real wallpaperHeroStackWidth: 440
readonly property real wallpaperHeroSplit: 0.55
readonly property real disabledOpacity: 0.38
readonly property real highlightBlend: 0.2
readonly property real bannerTextMinWidth: 100
@@ -1,171 +1,143 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Column {
Rectangle {
id: root
property string label: ""
property string badge: ""
property string path: ""
property string placeholderIcon: "image"
property string emptyText: I18n.tr("Not set", "wallpaper not set label")
property bool allowColor: true
property bool canCycle: false
readonly property bool isColor: path.startsWith("#")
readonly property bool isImage: path !== "" && !isColor
readonly property string fileName: path !== "" ? path.split("/").pop() : emptyText
signal browse
signal pickColor
signal clear
signal previous
signal next
spacing: Theme.spacingS
height: width * SettingsMetrics.wallpaperThumbRatio
radius: Theme.cornerRadiusM
color: isColor ? path : Theme.chipSurface
StyledText {
width: parent.width
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Theme.fontWeightMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
visible: root.label !== ""
ClippingRectangle {
anchors.fill: parent
radius: root.radius
color: "transparent"
Loader {
id: imageLoader
anchors.fill: parent
active: root.visible && (root.Window.window?.visible ?? false) && root.isImage
asynchronous: true
sourceComponent: CachingImage {
imagePath: root.path
animate: false
}
}
}
DankIcon {
anchors.centerIn: parent
name: root.placeholderIcon
size: Theme.iconSizeLarge
color: Theme.surfaceVariantText
visible: !root.isColor && imageLoader.item?.status !== Image.Ready
}
MouseArea {
anchors.fill: parent
onClicked: root.browse()
}
Rectangle {
id: frame
width: parent.width
height: width * SettingsMetrics.wallpaperThumbRatio
radius: Theme.cornerRadiusM
color: root.isColor ? root.path : Theme.chipSurface
anchors.fill: parent
radius: root.radius
color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
opacity: hoverArea.containsMouse ? 1 : 0
visible: opacity > 0
ClippingRectangle {
anchors.fill: parent
radius: frame.radius
color: "transparent"
Loader {
id: imageLoader
anchors.fill: parent
active: root.visible && (root.Window.window?.visible ?? false) && root.isImage
asynchronous: true
sourceComponent: CachingImage {
imagePath: root.path
animate: false
}
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
duration: SettingsMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
DankIcon {
Row {
anchors.centerIn: parent
name: root.placeholderIcon
size: Theme.iconSizeLarge
color: Theme.surfaceVariantText
visible: !root.isColor && imageLoader.item?.status !== Image.Ready
}
spacing: Theme.spacingS
MouseArea {
anchors.fill: parent
onClicked: root.browse()
}
Rectangle {
anchors.fill: parent
radius: frame.radius
color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
opacity: hoverArea.containsMouse ? 1 : 0
visible: opacity > 0
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
duration: SettingsMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "folder_open"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Browse")
onClicked: root.browse()
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "folder_open"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.surfaceText
Accessible.name: I18n.tr("Browse")
onClicked: root.browse()
}
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "palette"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.surfaceText
visible: root.allowColor
tooltipText: I18n.tr("Custom")
onClicked: root.pickColor()
}
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "close"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.error
visible: root.path !== ""
Accessible.name: I18n.tr("Clear")
onClicked: root.clear()
}
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "palette"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.surfaceText
visible: root.allowColor
tooltipText: I18n.tr("Custom")
onClicked: root.pickColor()
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "close"
iconSize: Theme.iconSizeMedium
backgroundColor: Theme.chipSurface
iconColor: Theme.error
visible: root.path !== ""
Accessible.name: I18n.tr("Clear")
onClicked: root.clear()
}
}
}
RowLayout {
width: parent.width
spacing: Theme.spacingS
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "skip_previous"
iconSize: Theme.iconSize
visible: root.canCycle
Accessible.name: I18n.tr("Previous")
onClicked: root.previous()
}
Rectangle {
id: chip
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingS
width: Math.min(chipText.implicitWidth + Theme.spacingS * 2, parent.width - Theme.spacingS * 2)
height: chipText.implicitHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusS
color: Theme.withAlpha(Theme.scrimColor, Theme.scrimAlpha)
StyledText {
Layout.fillWidth: true
text: root.path !== "" ? root.path.split("/").pop() : root.emptyText
id: chipText
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
text: root.badge !== "" ? root.badge + " · " + root.fileName : root.fileName
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
color: Theme.contrastLight
elide: Text.ElideMiddle
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
}
DankActionButton {
buttonSize: Theme.iconButtonSize
iconName: "skip_next"
iconSize: Theme.iconSize
visible: root.canCycle
Accessible.name: I18n.tr("Next")
onClicked: root.next()
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
}
}
+2 -4
View File
@@ -1,5 +1,6 @@
import QtQuick
import qs.Common
import "../Common/GridLayout.js" as GridUtils
QtObject {
id: root
@@ -56,10 +57,7 @@ QtObject {
if (!drag)
return;
const wanted = edgeFor(drag.step, drag.side) + px + popout.renderedAlignedX - drag.x;
const edge = edgeFor(drag.previewStep, drag.side);
const direction = (wanted - edge) * drag.side >= 0 ? 1 : -1;
const gain = Math.max(Math.abs(edgeFor(drag.previewStep + direction, drag.side) - edge), stepWidth / 2);
const step = Math.max(minStep, Math.min(maxStep, drag.previewStep + direction * Math.round(Math.abs(wanted - edge) / gain)));
const step = GridUtils.nearestStep(step => edgeFor(step, drag.side), drag.step, minStep, maxStep, wanted);
const rows = rowUnit > 0 ? Math.max(minRows, Math.min(maxRows, drag.rows + Math.round((py + popout.renderedAlignedY - drag.y) / rowUnit))) : drag.rows;
if (step === drag.previewStep && rows === drag.previewRows)
return;
+6
View File
@@ -66,3 +66,9 @@ test("a dragged tile snaps to the cell under its corner", () => {
assert.deepEqual(plain(grid.cellAt(grid.packCards(cards, order, 6, 600, 0, 100, true), 0, 0, 2, 1)), { col: 4, row: 0 });
assert.deepEqual(plain(grid.placedItems([{ id: "x" }, { id: "y" }], [null, { col: 1, row: 2 }])), [{ id: "x" }, { id: "y", col: 1, row: 2 }]);
});
test("panel step follows the pointer and holds on a pinned edge", () => {
const rightEdge = step => Math.min(1000, 500 + step * 36);
assert.equal(grid.nearestStep(rightEdge, 8, 6, 30, rightEdge(8) + 50), 9);
assert.equal(grid.nearestStep(rightEdge, 20, 6, 30, 1400), 20);
});
+2 -11
View File
@@ -1,6 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Modules.DankDash
@@ -80,11 +79,6 @@ ShellRoot {
return host.resizing && host.renderedAlignedX === host.alignedX;
}
function surfaceGeometry(host) {
const window = host.contentWindow;
return JSON.stringify([window.anchors.right, window.implicitWidth, window.WlrLayershell.margins.left]);
}
Component {
id: dashComponent
DankDashPopout {}
@@ -127,11 +121,9 @@ ShellRoot {
root.baseRows = c.panelRows;
check(root.baseRows >= DashMetrics.minimumTabRows && DashMetrics.storedPanelRows("overview") === 0, "unset rows fit the cards: " + root.baseRows);
check(!root.popout.contentWindow.anchors.right, "surface hugs the body outside edit mode");
const dashSurface = surfaceGeometry(root.popout);
root.popout.editMode = true;
check(root.popout.popupWidth === widthFor(DashMetrics.defaultGridColumns), "edit mode keeps the panel width");
check(surfaceGeometry(root.popout) === dashSurface, "entering edit mode preserves the bounded surface");
check(root.popout.contentWindow.implicitWidth >= root.popout.popupWidth + PopoutMetrics.editOverflow * 4, "surface reserves the edit handle padding");
check(!root.popout.contentWindow.anchors.right && root.popout.contentWindow.implicitWidth >= widthFor(root.popout.columnCap) + PopoutMetrics.editOverflow * 4, "edit mode holds a bounded surface wide enough for the column cap plus handle padding");
beginDrag(root.popout, c);
dragTo(root.popout, c, 2 * columnGain(), 0);
check(DashMetrics.panelPreview?.columns === 8, "preview snaps two columns wider: " + JSON.stringify(DashMetrics.panelPreview));
@@ -352,10 +344,9 @@ ShellRoot {
const ccContent = root.cc.contentLoader?.item ?? null;
check(root.cc.shouldBeVisible && ccContent, "control center open");
check(root.cc.popupWidth === CcMetrics.sheetWidthDefault, "control center default width");
const ccSurface = surfaceGeometry(root.cc);
root.cc.editMode = true;
check(root.cc.popupWidth === CcMetrics.sheetWidthDefault, "control center edit mode keeps the sheet width");
check(surfaceGeometry(root.cc) === ccSurface, "control center edit mode preserves the bounded surface");
check(!root.cc.contentWindow.anchors.right && root.cc.contentWindow.implicitWidth >= CcMetrics.sheetWidthFor(root.cc.gridColumnCap) + PopoutMetrics.editOverflow * 4, "control center edit mode holds a bounded surface wide enough for the column cap");
beginDrag(root.cc, ccContent);
dragTo(root.cc, ccContent, 1.4 * ccGain(), 0);
check(CcMetrics.gridColumns === CcMetrics.defaultColumns + 1 && CcMetrics.sheetWidth === CcMetrics.sheetWidthFor(CcMetrics.defaultColumns + 1), "control center preview snaps to a column: " + CcMetrics.sheetWidth);
@@ -105,6 +105,33 @@
"page": "notifications",
"icon": "palette"
},
{
"section": "wallpaper",
"label": "Appearance",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"appearance",
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"dark",
"desktop",
"hue",
"image",
"light",
"mode",
"palette",
"picture",
"theme",
"tint",
"wallpaper"
],
"page": "personalization"
},
{
"section": "_hub_applications",
"label": "Applications",
@@ -685,6 +712,34 @@
],
"page": "personalization"
},
{
"section": "colorMode",
"label": "Dark mode",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"dark",
"dark mode",
"desktop",
"hue",
"image",
"light",
"mode",
"night",
"palette",
"picture",
"theme",
"tint",
"wallpaper"
],
"page": "personalization"
},
{
"section": "iconThemeDark",
"label": "Dark mode theme",
@@ -2076,36 +2131,6 @@
],
"icon": "memory"
},
{
"section": "colorMode",
"label": "Theme & colors",
"tabIndex": -1,
"category": "Wallpaper & colors",
"keywords": [
"appearance",
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"dark",
"desktop",
"hue",
"image",
"light",
"look",
"mode",
"palette",
"picture",
"style",
"theme",
"tint",
"wallpaper"
],
"page": "personalization",
"icon": "schedule"
},
{
"section": "focusRingWidth",
"label": "Thickness",
@@ -2243,7 +2268,7 @@
"page": "personalization"
},
{
"section": "wallpaper",
"section": "wallpaperOptions",
"label": "Wallpaper",
"tabIndex": -1,
"category": "Wallpaper & colors",
@@ -2253,21 +2278,21 @@
"colors",
"colour",
"colours",
"dark",
"cycling",
"desktop",
"display",
"fill",
"hue",
"image",
"light",
"mode",
"monitor",
"palette",
"picture",
"tint",
"transition",
"wallpaper"
],
"page": "personalization",
"icon": "schedule"
"icon": "lock"
},
{
"section": "windowRadius",