diff --git a/src/browser/app/profile/features.inc b/src/browser/app/profile/features.inc index ade9013a7..83993709f 100644 --- a/src/browser/app/profile/features.inc +++ b/src/browser/app/profile/features.inc @@ -19,6 +19,9 @@ pref('zen.view.show-newtab-button-top', true); pref('zen.mediacontrols.enabled', true); +// Exposure: +pref('zen.haptic-feedback.enabled', true); + #ifdef MOZILLA_OFFICIAL pref('zen.rice.api.url', 'https://share.zen-browser.app', locked); pref('zen.injections.match-urls', 'https://zen-browser.app/*,https://share.zen-browser.app/*', locked); diff --git a/src/modules/libpref/init/zen-static-prefs.inc b/src/modules/libpref/init/zen-static-prefs.inc index 2761dbb22..982b531a2 100644 --- a/src/modules/libpref/init/zen-static-prefs.inc +++ b/src/modules/libpref/init/zen-static-prefs.inc @@ -11,3 +11,8 @@ value: 1 mirror: always #endif + +- name: zen.haptic-feedback.enabled + type: bool + value: true + mirror: always diff --git a/src/zen/common/styles/zen-panel-ui.css b/src/zen/common/styles/zen-panel-ui.css index bcefa1ea9..e71e8b143 100644 --- a/src/zen/common/styles/zen-panel-ui.css +++ b/src/zen/common/styles/zen-panel-ui.css @@ -62,5 +62,9 @@ panel[type='arrow'] { --panel-border-color: transparent; /* This should be kept in sync with GetMenuMaskImage() */ --panel-border-radius: 6px; + + &::part(content) { + background-color: light-dark(rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.2)) !important; + } } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index e2f6eb453..af479f49d 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -942,6 +942,9 @@ } removeTabContainersDragoverClass() { + if (this._dragIndicator) { + Services.zen.playHapticFeedback(); + } this.dragIndicator.remove(); this._dragIndicator = null; ZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open'); @@ -1043,16 +1046,19 @@ // Calculate middle to decide 'before' or 'after' const rect = targetTab.getBoundingClientRect(); - + let shouldPlayHapticFeedback = false; if (isVertical) { const separation = 8; const middleY = targetTab.screenY + rect.height / 2; const indicator = this.dragIndicator; let top = 0; if (event.screenY > middleY) { - top = rect.top + rect.height + 'px'; + top = Math.round(rect.top + rect.height) + 'px'; } else { - top = rect.top + 'px'; + top = Math.round(rect.top) + 'px'; + } + if (indicator.style.top !== top) { + shouldPlayHapticFeedback = true; } indicator.setAttribute('orientation', 'horizontal'); indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px'); @@ -1065,9 +1071,12 @@ const indicator = this.dragIndicator; let left = 0; if (event.screenX > middleX) { - left = rect.left + rect.width + 1 + 'px'; + left = Math.round(rect.left + rect.width + 1) + 'px'; } else { - left = rect.left - 2 + 'px'; + left = Math.round(rect.left - 2) + 'px'; + } + if (indicator.style.left !== left) { + shouldPlayHapticFeedback = true; } indicator.setAttribute('orientation', 'vertical'); indicator.style.setProperty('--indicator-top', rect.top + separation / 2 + 'px'); @@ -1075,6 +1084,9 @@ indicator.style.left = left; indicator.style.removeProperty('top'); } + if (shouldPlayHapticFeedback) { + Services.zen.playHapticFeedback(); + } } async onTabLabelChanged(tab) { diff --git a/src/zen/toolkit/common/ZenCommonUtils.cpp b/src/zen/toolkit/common/ZenCommonUtils.cpp index e4fa84e0d..154f3fbe7 100644 --- a/src/zen/toolkit/common/ZenCommonUtils.cpp +++ b/src/zen/toolkit/common/ZenCommonUtils.cpp @@ -11,6 +11,8 @@ #include "nsServiceManagerUtils.h" #include "nsISharePicker.h" +#include "mozilla/StaticPrefs_zen.h" + #if defined(XP_WIN) # include "mozilla/WindowsVersion.h" #endif @@ -57,11 +59,16 @@ using mozilla::dom::WindowGlobalChild; *canShare = false; \ return NS_OK; -/* - * @brief Check if the current context can share data. - * @param data The data to share. - * @returns True if the current context can share data, false otherwise. - */ +NS_IMETHODIMP +ZenCommonUtils::PlayHapticFeedback() { + // We don't have any haptic feedback on non-macOS platforms + // so we can just return. + if (!mozilla::StaticPrefs::zen_haptic_feedback_enabled()) { + return NS_OK; + } + return PlayHapticFeedbackInternal(); +} + NS_IMETHODIMP ZenCommonUtils::CanShare(bool* canShare) { auto aWindow = GetMostRecentWindow(); @@ -103,7 +110,7 @@ nsresult ZenCommonUtils::ShareInternal(nsCOMPtr& aWindow, ns auto ZenCommonUtils::IsSharingSupported() -> bool { #if defined(XP_WIN) && !defined(__MINGW32__) // The first public build that supports ShareCanceled API - return IsWindows10BuildOrLater(18956); + return mozilla::IsWindows10BuildOrLater(18956); #elif defined(NS_ZEN_CAN_SHARE_NATIVE) return NS_ZEN_CAN_SHARE_NATIVE; #else diff --git a/src/zen/toolkit/common/ZenCommonUtils.h b/src/zen/toolkit/common/ZenCommonUtils.h index 3f58702e7..229dc4a9a 100644 --- a/src/zen/toolkit/common/ZenCommonUtils.h +++ b/src/zen/toolkit/common/ZenCommonUtils.h @@ -32,17 +32,28 @@ class ZenCommonUtils final : public nsIZenCommonUtils { */ static auto IsSharingSupported() -> bool; /** - * @brief Helper function to share data via the native dialogs. - * @param aWindow The window to use for the share dialog. - * @param url The URL to share. - * @param title The title of the share. - * @param text The text to share. - * @returns A promise that resolves when the share is complete. - */ + * @brief Helper function to share data via the native dialogs. + * @param aWindow The window to use for the share dialog. + * @param url The URL to share. + * @param title The title of the share. + * @param text The text to share. + * @returns A promise that resolves when the share is complete. + */ static auto ShareInternal(nsCOMPtr& aWindow, nsIURI* url, const nsACString& title, const nsACString& text, uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight) -> nsresult; + /** + * @brief Helper function to play haptic feedback. + */ +#if !defined(XP_MACOSX) + static auto PlayHapticFeedbackInternal() -> nsresult { + // No-op on non-macOS platforms + return NS_ERROR_NOT_IMPLEMENTED; + } +#else + static auto PlayHapticFeedbackInternal() -> nsresult; +#endif }; } // namespace zen diff --git a/src/zen/toolkit/common/cocoa/ZenHapticFeedback.mm b/src/zen/toolkit/common/cocoa/ZenHapticFeedback.mm new file mode 100644 index 000000000..602e9f396 --- /dev/null +++ b/src/zen/toolkit/common/cocoa/ZenHapticFeedback.mm @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ZenCommonUtils.h" +#include "nsCocoaUtils.h" + +#include "nsPIDOMWindow.h" +#include "WidgetUtils.h" +#include "nsIWidget.h" + +extern mozilla::LazyLogModule gCocoaUtilsLog; +#undef LOG +#define LOG(...) MOZ_LOG(gCocoaUtilsLog, mozilla::LogLevel::Info, (__VA_ARGS__)) + +#import +#import + +namespace zen { + +nsresult ZenCommonUtils::PlayHapticFeedbackInternal() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + if (@available(macOS 10.14, *)) { + id performer = [NSHapticFeedbackManager defaultPerformer]; + [performer performFeedbackPattern:NSHapticFeedbackPatternAlignment + performanceTime:NSHapticFeedbackPerformanceTimeDefault]; + } else { + // Fallback on earlier versions + // Note: This is a no-op on older versions of iOS/macOS + } + return NS_OK; + NS_OBJC_END_TRY_BLOCK_RETURN(NS_OK); +} + +} diff --git a/src/zen/toolkit/common/cocoa/ZenShareInternal.mm b/src/zen/toolkit/common/cocoa/ZenShareInternal.mm index eb47cb62d..ae5bd5577 100644 --- a/src/zen/toolkit/common/cocoa/ZenShareInternal.mm +++ b/src/zen/toolkit/common/cocoa/ZenShareInternal.mm @@ -74,5 +74,6 @@ auto nsZenNativeShareInternal::ShowNativeDialog(nsCOMPtr& aW [sharingPicker showRelativeToRect:rect ofView:cocoaMru.contentView preferredEdge:NSMaxYEdge]; + [sharingPicker release]; return NS_OK; } diff --git a/src/zen/toolkit/common/cocoa/moz.build b/src/zen/toolkit/common/cocoa/moz.build index 004530c38..92a8d7444 100644 --- a/src/zen/toolkit/common/cocoa/moz.build +++ b/src/zen/toolkit/common/cocoa/moz.build @@ -1,6 +1,7 @@ FINAL_LIBRARY = "xul" SOURCES += [ + "ZenHapticFeedback.mm", "ZenShareInternal.mm", ] @@ -9,4 +10,4 @@ LOCAL_INCLUDES += [ "/widget", "/widget/cocoa", "/xpcom/base", -] \ No newline at end of file +] diff --git a/src/zen/toolkit/common/nsIZenCommonUtils.idl b/src/zen/toolkit/common/nsIZenCommonUtils.idl index 06eb62ea8..1ceb714c2 100644 --- a/src/zen/toolkit/common/nsIZenCommonUtils.idl +++ b/src/zen/toolkit/common/nsIZenCommonUtils.idl @@ -28,5 +28,9 @@ interface nsIZenCommonUtils : nsISupports { * @returns True if the current context can share data, false otherwise. */ boolean canShare(); + /* + * @brief Play a single haptic feedback note if supported. + */ + void playHapticFeedback(); }; diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs index 959f1dc11..75accb61f 100644 --- a/src/zen/workspaces/ZenGradientGenerator.mjs +++ b/src/zen/workspaces/ZenGradientGenerator.mjs @@ -267,6 +267,7 @@ } if (previousTexture !== this.currentTexture) { this.updateCurrentWorkspace(); + Services.zen.playHapticFeedback(); } } diff --git a/src/zen/workspaces/zen-gradient-generator.css b/src/zen/workspaces/zen-gradient-generator.css index da4330584..90c22c287 100644 --- a/src/zen/workspaces/zen-gradient-generator.css +++ b/src/zen/workspaces/zen-gradient-generator.css @@ -180,7 +180,7 @@ } &::-moz-range-track { - background: var(--zen-colors-border); + background: light-dark(rgba(0, 0, 0, 0.3), rgba(255, 255, 255, 0.3)); border-radius: 999px; height: 6px; } @@ -225,9 +225,9 @@ transform: translate(-50%, -50%); &:first-of-type { - width: 30px; - height: 30px; - border-width: 3px; + width: 36px; + height: 36px; + border-width: 4px; z-index: 2; transition: transform 0.1s; &:hover { @@ -264,7 +264,7 @@ transform: translateX(-50%); & .separator, & #PanelUI-zen-gradient-generator-color-toggle-algo { - background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.5)); + background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.15)); } & button { @@ -356,13 +356,13 @@ height: 4px; width: 4px; border-radius: 50%; - background: light-dark(rgba(0, 0, 0, 0.3), rgba(255, 255, 255, 0.2)); + background: light-dark(rgba(0, 0, 0, 0.3), rgba(255, 255, 255, 0.3)); position: absolute; transition: opacity 0.2s; transform: translate(-50%, -50%); pointer-events: none; &:not(.active) { - opacity: 0.2; + opacity: 0.4; } }