diff --git a/AGENTS.md b/AGENTS.md index 09451fbf..ff32382b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,7 @@ Ambxst is a highly customizable Wayland shell built with Quickshell. It provides | `AxctlService` | Singleton | `modules/services/AxctlService.qml` | Compositor abstraction (focus, dispatch) | | `StateService` | Singleton | `modules/services/StateService.qml` | JSON persistence for session state | | `FocusGrabManager` | Singleton | `modules/services/FocusGrabManager.qml` | Input focus coordination | +| `SafeLoader` | Component | `modules/components/SafeLoader.qml` | Loader with error handling and fallback UI | ## CONVENTIONS - **Singletons**: `pragma Singleton` + `Singleton { id: root }` for all services and global state. @@ -114,6 +115,7 @@ Ambxst is a highly customizable Wayland shell built with Quickshell. It provides ```bash # Run shell (requires Quickshell + Hyprland) qs -p shell.qml + # Or via CLI wrapper: ./cli.sh diff --git a/cli.sh b/cli.sh index 98274b12..39f65b59 100755 --- a/cli.sh +++ b/cli.sh @@ -20,6 +20,27 @@ if [ -n "${QML2_IMPORT_PATH:-}" ] && [ -z "${QML_IMPORT_PATH:-}" ]; then export QML_IMPORT_PATH="$QML2_IMPORT_PATH" fi +# Qt Quick GPU acceleration settings (user overrides preserved) +# Vulkan preferred for both AMD and NVIDIA (requires mesa/26.0+ or NVIDIA 525+) +# Falls back gracefully on older drivers. +# We probe with 'vulkaninfo --summary' rather than 'glxinfo' because glxinfo +# only indicates OpenGL/GLX availability — it does NOT confirm a Vulkan ICD +# is present. Setting QT_QUICK_BACKEND=vulkan without a working ICD causes Qt +# to silently fall back to software rendering. +if [[ -z "${QT_QUICK_BACKEND:-}" ]]; then + if vulkaninfo --summary >/dev/null 2>&1; then + export QT_QUICK_BACKEND=vulkan + else + export QT_QUICK_BACKEND=opengl + fi +fi + +# Faster QML array operations +[[ -z "${QML_USE_TYPED_ARRAYS:-}" ]] && export QML_USE_TYPED_ARRAYS=1 + +# Multi-threaded rendering for smoother UI +[[ -z "${QT_THREADED_RENDERER:-}" ]] && export QT_THREADED_RENDERER=1 + # Ensure config files exist - copy from preset if missing ensure_config_files() { local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ambxst/config" diff --git a/install.sh b/install.sh index 22c36696..24d94e17 100755 --- a/install.sh +++ b/install.sh @@ -129,6 +129,7 @@ install_dependencies() { google-roboto-fonts google-roboto-mono-fonts dejavu-sans-fonts liberation-fonts google-noto-fonts-common google-noto-cjk-fonts google-noto-emoji-fonts mpvpaper matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl + vulkan-loader ) log_info "Installing dependencies..." @@ -177,6 +178,7 @@ install_dependencies() { ttf-nerd-fonts-symbols matugen gpu-screen-recorder wl-clip-persist mpvpaper gradia quickshell ttf-phosphor-icons ttf-league-gothic adw-gtk-theme + vulkan-icd-loader ) log_info "Installing dependencies with $AUR_HELPER..." diff --git a/modules/bar/BarContent.qml b/modules/bar/BarContent.qml index b9e57ca7..6a1870ac 100644 --- a/modules/bar/BarContent.qml +++ b/modules/bar/BarContent.qml @@ -172,8 +172,8 @@ Item { readonly property int leftOuterMargin: (orientation === "horizontal" || barPosition === "left") ? barBg.outerMargin : 0 readonly property int rightOuterMargin: (orientation === "horizontal" || barPosition === "right") ? barBg.outerMargin : 0 - readonly property int contentImplicitWidth: orientation === "horizontal" ? (horizontalLoader.item && horizontalLoader.item.implicitWidth !== undefined ? horizontalLoader.item.implicitWidth : 0) : (verticalLoader.item && verticalLoader.item.implicitWidth !== undefined ? verticalLoader.item.implicitWidth : 0) - readonly property int contentImplicitHeight: orientation === "horizontal" ? (horizontalLoader.item && horizontalLoader.item.implicitHeight !== undefined ? horizontalLoader.item.implicitHeight : 0) : (verticalLoader.item && verticalLoader.item.implicitHeight !== undefined ? verticalLoader.item.implicitHeight : 0) + readonly property int contentImplicitWidth: orientation === "horizontal" ? (horizontalLoader.item ? horizontalLoader.item.implicitWidth : 0) : (verticalLoader.item ? verticalLoader.item.implicitWidth : 0) + readonly property int contentImplicitHeight: orientation === "horizontal" ? (horizontalLoader.item ? horizontalLoader.item.implicitHeight : 0) : (verticalLoader.item ? verticalLoader.item.implicitHeight : 0) readonly property int barTargetWidth: orientation === "vertical" ? (contentImplicitWidth + 2 * barPadding) : 0 readonly property int barTargetHeight: orientation === "horizontal" ? (contentImplicitHeight + 2 * barPadding) : 0 diff --git a/modules/bar/systray/SysTrayItem.qml b/modules/bar/systray/SysTrayItem.qml index 0b725b1a..a13110fd 100644 --- a/modules/bar/systray/SysTrayItem.qml +++ b/modules/bar/systray/SysTrayItem.qml @@ -174,7 +174,7 @@ MouseArea { StyledToolTip { show: root.isHovered tooltipText: root.item.tooltipTitle || root.item.title - desciription: root.item.tooltipDescription || "" + description: root.item.tooltipDescription || "" } HoverHandler { diff --git a/modules/components/SafeLoader.qml b/modules/components/SafeLoader.qml new file mode 100644 index 00000000..f58bac79 --- /dev/null +++ b/modules/components/SafeLoader.qml @@ -0,0 +1,80 @@ +import QtQuick + +/** + * SafeLoader - A Loader with built-in error handling and fallback UI + * Use this instead of plain Loader for components that may fail to load + */ +Item { + id: root + + property var sourceComponent + property string placeholderText: "Loading..." + property bool showPlaceholder: true + property color placeholderColor: "#808080" + property var fallbackItem + + // Internal loader — not exposed as public API to prevent callers from + // bypassing root.sourceComponent and writing to it directly. + Loader { + id: internalLoader + anchors.fill: parent + sourceComponent: root.sourceComponent + asynchronous: true + + onStatusChanged: { + if (internalLoader.status === Loader.Error) { + console.error("[SafeLoader] Failed to load component:", internalLoader.errorString); + root.handleError(internalLoader.errorString); + } + } + } + + signal loadError(string errorString) + + function handleError(errorString) { + root.loadError(errorString); + } + + // Read-only state properties + readonly property bool isLoading: internalLoader.status === Loader.Loading + readonly property bool hasError: internalLoader.status === Loader.Error + readonly property bool isReady: internalLoader.status === Loader.Ready + + // Fallback content when loading or error + StyledRect { + id: placeholder + anchors.fill: parent + visible: root.showPlaceholder && (root.isLoading || root.hasError) && !root.fallbackItem + color: root.hasError ? "#20000000" : "transparent" + + Text { + anchors.centerIn: parent + text: root.hasError ? "Failed to load" : root.placeholderText + color: root.placeholderColor + font.pixelSize: 14 + } + } + + // Container for a caller-supplied fallback item; shown on error only. + Item { + id: fallbackContainer + anchors.fill: parent + visible: root.fallbackItem !== null && root.hasError + z: 10 + } + + // Reparent a caller-supplied item into the fallback container so it + // becomes visible when the load fails. + function setFallback(item) { + root.fallbackItem = item; + item.parent = fallbackContainer; + } + + // Retry loading by null-cycling sourceComponent (standard QML pattern). + function retry() { + var current = root.sourceComponent; + internalLoader.sourceComponent = null; + internalLoader.sourceComponent = current; + } +} + diff --git a/modules/components/StyledToolTip.qml b/modules/components/StyledToolTip.qml index 8f4d5165..caaaf79a 100644 --- a/modules/components/StyledToolTip.qml +++ b/modules/components/StyledToolTip.qml @@ -9,11 +9,11 @@ import qs.modules.components ToolTip { id: root property string tooltipText: "" - property string desciription: "" + property string description: "" property bool show: false text: tooltipText - delay: 1000 + delay: 700 timeout: -1 visible: show && tooltipText.length > 0 @@ -28,16 +28,16 @@ ToolTip { Text { text: root.tooltipText color: Colors.overBackground - font.pixelSize: Config.theme.fontSize + font.pixelSize: Styling.fontSize(0) font.weight: Font.Bold font.family: Config.theme.font } Text { - text: root.desciription - visible: root.desciription.length > 0 - color: Colors.overBackground - font.pixelSize: Config.theme.fontSize - 2 + text: root.description + visible: root.description.length > 0 + color: Qt.rgba(Colors.overBackground.r, Colors.overBackground.g, Colors.overBackground.b, 0.7) + font.pixelSize: Styling.fontSize(-2) font.family: Config.theme.font } } diff --git a/modules/lockscreen/LockScreen.qml b/modules/lockscreen/LockScreen.qml index e3f48446..09530d25 100644 --- a/modules/lockscreen/LockScreen.qml +++ b/modules/lockscreen/LockScreen.qml @@ -147,7 +147,7 @@ WlSessionLockSurface { id: clockContainer anchors.centerIn: parent width: clockRow.width - height: hoursText.height + (hoursText.height * 0.5) + height: hoursText.height + (hoursText.height * 0.5) + 48 z: 10 property date currentTime: new Date() @@ -275,7 +275,8 @@ WlSessionLockSurface { repeat: true onTriggered: clockContainer.currentTime = new Date() } - } + + } // Music player (slides from left) Item { @@ -412,6 +413,7 @@ WlSessionLockSurface { smooth: true asynchronous: true visible: status === Image.Ready + sourceSize: Qt.size(128, 128) layer.enabled: true layer.effect: MultiEffect { diff --git a/modules/notch/NotchAnimationBehavior.qml b/modules/notch/NotchAnimationBehavior.qml index 1e57e3e8..4b28fc65 100644 --- a/modules/notch/NotchAnimationBehavior.qml +++ b/modules/notch/NotchAnimationBehavior.qml @@ -1,18 +1,30 @@ import QtQuick import qs.config -// Comportamiento estándar para animaciones de elementos que aparecen en el notch +// Standard animation behavior for elements appearing inside the notch Item { id: root - // Propiedad para controlar la visibilidad con animaciones + // Controls visibility with animated entrance/exit property bool isVisible: false - // Aplicar las animaciones estándar del notch - scale: isVisible ? 1.0 : 0.8 + // Scale + opacity entrance — scale pops from 0.85 with OutBack overshoot + scale: isVisible ? 1.0 : 0.85 opacity: isVisible ? 1.0 : 0.0 visible: opacity > 0 + // Subtle vertical translate: drop in from slightly above when appearing + property real slideOffset: isVisible ? 0 : -5 + transform: Translate { y: root.slideOffset } + + Behavior on slideOffset { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutQuart + } + } + Behavior on scale { enabled: Config.animDuration > 0 NumberAnimation { diff --git a/modules/shell/osd/OSD.qml b/modules/shell/osd/OSD.qml index 432f771d..75214170 100644 --- a/modules/shell/osd/OSD.qml +++ b/modules/shell/osd/OSD.qml @@ -28,7 +28,10 @@ PanelWindow { color: "transparent" - visible: GlobalStates.osdVisible + // Keep the PanelWindow alive whenever visible OR while the exit animation + // is still playing (opacity > 0). Binding only to GlobalStates.osdVisible + // would hide the window immediately on dismiss, cutting the fade-out dead. + visible: GlobalStates.osdVisible || osdRect.opacity > 0 // Internal state for responsiveness property real osdValue: 0 @@ -43,17 +46,38 @@ PanelWindow { variant: "popup" anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - implicitWidth: 220 - implicitHeight: 52 + implicitWidth: 240 + implicitHeight: 56 radius: Styling.radius(16) + // Slide-in from below when visible, slide out down when hiding + property real slideY: GlobalStates.osdVisible ? 0 : 16 + transform: Translate { y: osdRect.slideY } + + Behavior on slideY { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutQuart + } + } + + opacity: GlobalStates.osdVisible ? 1.0 : 0.0 + Behavior on opacity { + enabled: Config.animDuration > 0 + NumberAnimation { + duration: Config.animDuration + easing.type: Easing.OutQuart + } + } + RowLayout { anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 24 + anchors.leftMargin: 14 + anchors.rightMargin: 16 anchors.topMargin: 8 anchors.bottomMargin: 8 - spacing: 14 + spacing: 12 Text { id: iconText @@ -67,7 +91,7 @@ PanelWindow { } } font.family: Icons.font - font.pixelSize: 22 + font.pixelSize: 20 color: Colors.overBackground Layout.alignment: Qt.AlignVCenter @@ -111,23 +135,26 @@ PanelWindow { return ""; } font.family: Config.theme.font - font.pixelSize: 15 + font.pixelSize: Styling.fontSize(0) font.bold: false color: Colors.overBackground Layout.alignment: Qt.AlignBottom } - Item { - Layout.fillWidth: true - } + Item { Layout.fillWidth: true } Text { - text: Math.round(root.osdValue * 100) + // Show percentage value; clamp display to avoid "-0%" + text: Math.max(0, Math.round(root.osdValue * 100)) + "%" font.family: Config.theme.font - font.pixelSize: 15 + font.pixelSize: Styling.fontSize(0) font.bold: false color: Colors.overBackground Layout.alignment: Qt.AlignBottom + + Behavior on text { + enabled: false // text changes should be instant + } } } @@ -147,19 +174,18 @@ PanelWindow { } } - // Close on click or hover + // Hovering pauses the auto-hide timer; clicking dismisses immediately MouseArea { anchors.fill: parent - onEntered: { - hideTimer.stop(); - hideTimer.triggered(); - } hoverEnabled: true + onEntered: hideTimer.stop() + onExited: hideTimer.restart() + onClicked: GlobalStates.osdVisible = false } Timer { id: hideTimer - interval: 2500 + interval: 3000 onTriggered: GlobalStates.osdVisible = false } @@ -172,7 +198,7 @@ PanelWindow { } } - // Services connections - Direct and responsive + // Service connections — direct and responsive Connections { target: Audio function onVolumeChanged(volume, muted, node) { @@ -194,7 +220,7 @@ PanelWindow { Connections { target: Brightness function onBrightnessChanged(value, screen) { - // Check if the change happened on THIS screen or if it's a sync change + // Only react if the change is for this screen, or synced across all screens if (!screen || !root.targetScreen || screen.name === root.targetScreen.name || Brightness.syncBrightness) { root.osdValue = value; root.osdMuted = false; diff --git a/modules/sidebar/AssistantSidebar.qml b/modules/sidebar/AssistantSidebar.qml index be201de3..58b16105 100644 --- a/modules/sidebar/AssistantSidebar.qml +++ b/modules/sidebar/AssistantSidebar.qml @@ -743,7 +743,7 @@ Item { ClippingRectangle { anchors.fill: parent radius: Styling.radius(16) - color: Colors.surfaceDim + color: "transparent" visible: isUser Image { diff --git a/modules/widgets/dashboard/controls/PanelTitlebar.qml b/modules/widgets/dashboard/controls/PanelTitlebar.qml index 1113019a..a5b0787e 100644 --- a/modules/widgets/dashboard/controls/PanelTitlebar.qml +++ b/modules/widgets/dashboard/controls/PanelTitlebar.qml @@ -57,7 +57,7 @@ RowLayout { id: customContentContainer Layout.preferredWidth: childrenRect.width Layout.preferredHeight: childrenRect.height - visible: children.length > 0 + visible: root.customContent && root.customContent.length > 0 } // Action buttons diff --git a/modules/widgets/dashboard/metrics/MetricsTab.qml b/modules/widgets/dashboard/metrics/MetricsTab.qml index 97b91b37..5bb774a9 100644 --- a/modules/widgets/dashboard/metrics/MetricsTab.qml +++ b/modules/widgets/dashboard/metrics/MetricsTab.qml @@ -179,12 +179,12 @@ Rectangle { spacing: 16 // User avatar - StyledRect { + Rectangle { id: avatarContainer Layout.preferredWidth: 96 Layout.preferredHeight: 96 radius: Config.roundness > 0 ? (height / 2) * (Config.roundness / 16) : 0 - variant: "primary" + color: "transparent" Image { id: userAvatar @@ -195,6 +195,7 @@ Rectangle { smooth: true asynchronous: true visible: status === Image.Ready + sourceSize: Qt.size(192, 192) layer.enabled: true layer.effect: MultiEffect { diff --git a/modules/widgets/defaultview/UserInfo.qml b/modules/widgets/defaultview/UserInfo.qml index 1cfad227..b19cece2 100644 --- a/modules/widgets/defaultview/UserInfo.qml +++ b/modules/widgets/defaultview/UserInfo.qml @@ -59,11 +59,13 @@ Item { height: 24 radius: Styling.radius(0) clip: true + color: "transparent" Image { anchors.fill: parent source: `file://${Quickshell.env("HOME")}/.face.icon` fillMode: Image.PreserveAspectCrop + sourceSize: Qt.size(48, 48) } } }