From 62b4656694449c2cb409b64e9dbabc2a054f0974 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Wed, 8 Jul 2026 00:46:07 +0800 Subject: [PATCH] feat(dark-mode): add force dark mode control and website highlight --- src/dao/browser/dao_pref_names.cc | 105 +++++++++ src/dao/browser/dao_pref_names.h | 27 +++ src/dao/browser/strings/dao_strings.grd | 15 ++ .../ui/views/dao_browser_browsertest.cc | 207 ++++++++++++++++++ .../ui/views/dao_control_center_more_menu.cc | 17 ++ .../ui/views/dao_control_center_more_menu.h | 4 +- .../ui/views/dao_control_center_popup.cc | 44 +++- .../ui/views/dao_control_center_popup.h | 6 + .../dao_control_center_utility_section.cc | 188 +++++++++++----- .../dao_control_center_utility_section.h | 12 +- src/dao/browser/ui/views/dao_lucide_icons.cc | 20 ++ src/dao/browser/ui/views/dao_lucide_icons.h | 1 + ...nt_browser_client_force_dark_mode.cc.patch | 54 +++++ website/components/FeatureGrid.module.css | 22 ++ website/components/FeatureGrid.tsx | 9 +- website/components/ui/LucideIcon.tsx | 6 + .../lib/__tests__/homepage-features.test.ts | 18 ++ 17 files changed, 698 insertions(+), 57 deletions(-) create mode 100644 src/patches/chrome/browser/chrome_content_browser_client_force_dark_mode.cc.patch create mode 100644 website/lib/__tests__/homepage-features.test.ts diff --git a/src/dao/browser/dao_pref_names.cc b/src/dao/browser/dao_pref_names.cc index 82b03fe4..0effa82c 100644 --- a/src/dao/browser/dao_pref_names.cc +++ b/src/dao/browser/dao_pref_names.cc @@ -4,7 +4,17 @@ #include "dao/browser/dao_pref_names.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_window/public/browser_window_interface.h" +#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/web_preferences/web_preferences.h" +#include "third_party/blink/public/mojom/css/preferred_color_scheme.mojom.h" +#include "ui/native_theme/native_theme.h" namespace dao::prefs { @@ -15,6 +25,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { registry->RegisterBooleanPref(kDaoEnhancedPipEnabled, true); registry->RegisterBooleanPref(kDaoWelcomeShown, false); registry->RegisterBooleanPref(kDaoLittleDaoEnabled, true); + registry->RegisterBooleanPref(kDaoForceDarkModeEnabled, false); registry->RegisterBooleanPref(kDaoEnhancedCommandBarSuggestionsEnabled, false); registry->RegisterDictionaryPref(kDaoLittleDaoWindowSize); @@ -24,3 +35,97 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { } } // namespace dao::prefs + +namespace dao { + +namespace { + +Profile* GetStorageProfile(Profile* profile) { + return profile ? profile->GetOriginalProfile() : nullptr; +} + +bool UsesSameStorageProfile(Profile* lhs, Profile* rhs) { + return GetStorageProfile(lhs) == GetStorageProfile(rhs); +} + +} // namespace + +bool IsSystemDarkMode() { + ui::NativeTheme* theme = ui::NativeTheme::GetInstanceForNativeUi(); + return theme && theme->preferred_color_scheme() == + ui::NativeTheme::PreferredColorScheme::kDark; +} + +bool IsForceDarkModeUserEnabled(Profile* profile) { + Profile* storage_profile = GetStorageProfile(profile); + return storage_profile && storage_profile->GetPrefs()->GetBoolean( + prefs::kDaoForceDarkModeEnabled); +} + +bool IsForceDarkModeAvailable() { + return IsSystemDarkMode(); +} + +bool IsForceDarkModeEffective(Profile* profile) { + return IsForceDarkModeAvailable() && IsForceDarkModeUserEnabled(profile); +} + +void SetForceDarkModeUserEnabled(Profile* profile, bool enabled) { + Profile* storage_profile = GetStorageProfile(profile); + if (!storage_profile) { + return; + } + storage_profile->GetPrefs()->SetBoolean(prefs::kDaoForceDarkModeEnabled, + enabled); + NotifyForceDarkModeChanged(storage_profile); +} + +void ApplyForceDarkModePreferences( + Profile* profile, + blink::web_pref::WebPreferences* web_preferences) { + if (!web_preferences) { + return; + } + + const bool force_dark_mode = IsForceDarkModeEffective(profile); + web_preferences->force_dark_mode_enabled = force_dark_mode; + if (!force_dark_mode) { + return; + } + + web_preferences->preferred_color_scheme = + blink::mojom::PreferredColorScheme::kDark; + web_preferences->preferred_root_scrollbar_color_scheme = + blink::mojom::PreferredColorScheme::kDark; +} + +void NotifyForceDarkModeChanged(Profile* profile) { + Profile* storage_profile = GetStorageProfile(profile); + if (!storage_profile) { + return; + } + + for (BrowserWindowInterface* browser_window : + GetAllBrowserWindowInterfaces()) { + Browser* browser = + browser_window ? browser_window->GetBrowserForMigrationOnly() : nullptr; + if (!browser || + !UsesSameStorageProfile(browser->profile(), storage_profile)) { + continue; + } + + TabStripModel* tab_strip_model = browser->tab_strip_model(); + if (!tab_strip_model) { + continue; + } + + for (int i = 0; i < tab_strip_model->count(); ++i) { + content::WebContents* contents = tab_strip_model->GetWebContentsAt(i); + if (contents) { + contents->OnWebPreferencesChanged(); + } + } + } +} + +} // namespace dao diff --git a/src/dao/browser/dao_pref_names.h b/src/dao/browser/dao_pref_names.h index 73969e05..16663189 100644 --- a/src/dao/browser/dao_pref_names.h +++ b/src/dao/browser/dao_pref_names.h @@ -5,6 +5,12 @@ #ifndef DAO_BROWSER_DAO_PREF_NAMES_H_ #define DAO_BROWSER_DAO_PREF_NAMES_H_ +class Profile; + +namespace blink::web_pref { +struct WebPreferences; +} + namespace user_prefs { class PrefRegistrySyncable; } @@ -40,6 +46,12 @@ inline constexpr char kDaoWelcomeShown[] = "dao.welcome_shown"; // When false, external links use the regular full browser window. inline constexpr char kDaoLittleDaoEnabled[] = "dao.little_dao_enabled"; +// Boolean pref that remembers whether Dao should use Chromium's Auto Dark Mode +// pipeline for web contents. It only takes effect while the system appearance +// is dark. +inline constexpr char kDaoForceDarkModeEnabled[] = + "dao.force_dark_mode_enabled"; + // Boolean pref that enables the richer Arc-style command bar suggestion // pipeline. Kept off by default while the ranking model is still maturing. inline constexpr char kDaoEnhancedCommandBarSuggestionsEnabled[] = @@ -68,4 +80,19 @@ inline constexpr char kDaoDreamExcludedDomains[] = } // namespace dao::prefs +namespace dao { + +bool IsSystemDarkMode(); +bool IsForceDarkModeUserEnabled(Profile* profile); +bool IsForceDarkModeAvailable(); +bool IsForceDarkModeEffective(Profile* profile); + +void SetForceDarkModeUserEnabled(Profile* profile, bool enabled); +void ApplyForceDarkModePreferences( + Profile* profile, + blink::web_pref::WebPreferences* web_preferences); +void NotifyForceDarkModeChanged(Profile* profile); + +} // namespace dao + #endif // DAO_BROWSER_DAO_PREF_NAMES_H_ diff --git a/src/dao/browser/strings/dao_strings.grd b/src/dao/browser/strings/dao_strings.grd index b8a1a6d0..19ae94bf 100644 --- a/src/dao/browser/strings/dao_strings.grd +++ b/src/dao/browser/strings/dao_strings.grd @@ -336,6 +336,9 @@ Back + + Share + Clear Cache @@ -345,6 +348,18 @@ Clearing... + + Force dark mode + + + Dark + + + Darken websites with Auto Dark Mode while the system is in dark mode. + + + Available when the system is in dark mode. + diff --git a/src/dao/browser/ui/views/dao_browser_browsertest.cc b/src/dao/browser/ui/views/dao_browser_browsertest.cc index dbc0cffd..9aec64ea 100644 --- a/src/dao/browser/ui/views/dao_browser_browsertest.cc +++ b/src/dao/browser/ui/views/dao_browser_browsertest.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include +#include #include "base/files/file_path.h" #include "base/files/file_util.h" @@ -114,6 +115,7 @@ #include "dao/browser/ui/views/dao_control_center_more_menu.h" #include "dao/browser/ui/views/dao_control_center_popup.h" #include "dao/browser/ui/views/dao_control_center_qr_view.h" +#include "dao/browser/ui/views/dao_control_center_utility_section.h" #include "dao/browser/ui/views/dao_corner_overlay_view.h" #include "dao/browser/ui/views/dao_load_progress_view.h" #include "dao/browser/ui/views/dao_pinned_extensions_container.h" @@ -142,6 +144,8 @@ #include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/common/web_preferences/web_preferences.h" +#include "third_party/blink/public/mojom/css/preferred_color_scheme.mojom.h" #include "third_party/blink/public/mojom/devtools/console_message.mojom-shared.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkColor.h" @@ -160,6 +164,7 @@ #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_rep.h" #include "ui/native_theme/native_theme.h" +#include "ui/views/controls/button/checkbox.h" #include "ui/views/controls/button/image_button.h" #include "ui/views/controls/button/label_button.h" #include "ui/views/controls/button/md_text_button.h" @@ -340,6 +345,36 @@ views::Button* FindButtonWithAccessibleName( return nullptr; } +std::vector GetDirectButtonAccessibleNames(views::View* root) { + std::vector names; + if (!root) { + return names; + } + for (views::View* child : root->children()) { + if (auto* button = views::AsViewClass(child)) { + names.push_back(button->GetAccessibleName()); + } + } + return names; +} + +views::LabelButton* FindLabelButtonWithText(views::View* root, + std::u16string_view text) { + if (!root) { + return nullptr; + } + if (auto* button = views::AsViewClass(root); + button && button->GetText() == text) { + return button; + } + for (views::View* child : root->children()) { + if (auto* button = FindLabelButtonWithText(child, text)) { + return button; + } + } + return nullptr; +} + views::LabelButton* FindLabelButtonExceptText(views::View* root, std::u16string_view text) { if (!root) { @@ -5009,6 +5044,147 @@ IN_PROC_BROWSER_TEST_F(DaoToastViewBrowserTest, ShowToastMakesVisible) { using DaoControlCenterPopupBrowserTest = InProcessBrowserTest; +IN_PROC_BROWSER_TEST_F(DaoControlCenterPopupBrowserTest, + ForceDarkModeDefaultsOffAndRequiresSystemDark) { + Profile* profile = browser()->profile(); + PrefService* prefs = profile->GetPrefs(); + ASSERT_TRUE(prefs); + EXPECT_FALSE(prefs->GetBoolean(prefs::kDaoForceDarkModeEnabled)); + + ui::NativeTheme* theme = ui::NativeTheme::GetInstanceForNativeUi(); + ASSERT_TRUE(theme); + theme->set_preferred_color_scheme( + ui::NativeTheme::PreferredColorScheme::kDark); + EXPECT_FALSE(IsForceDarkModeEffective(profile)); + blink::web_pref::WebPreferences web_preferences; + web_preferences.force_dark_mode_enabled = true; + ApplyForceDarkModePreferences(profile, &web_preferences); + EXPECT_FALSE(web_preferences.force_dark_mode_enabled); + + prefs->SetBoolean(prefs::kDaoForceDarkModeEnabled, true); + EXPECT_TRUE(IsForceDarkModeEffective(profile)); + ApplyForceDarkModePreferences(profile, &web_preferences); + EXPECT_TRUE(web_preferences.force_dark_mode_enabled); + EXPECT_EQ(blink::mojom::PreferredColorScheme::kDark, + web_preferences.preferred_color_scheme); + EXPECT_EQ(blink::mojom::PreferredColorScheme::kDark, + web_preferences.preferred_root_scrollbar_color_scheme); + + theme->set_preferred_color_scheme( + ui::NativeTheme::PreferredColorScheme::kLight); + EXPECT_FALSE(IsForceDarkModeEffective(profile)); + ApplyForceDarkModePreferences(profile, &web_preferences); + EXPECT_FALSE(web_preferences.force_dark_mode_enabled); + EXPECT_TRUE(prefs->GetBoolean(prefs::kDaoForceDarkModeEnabled)); + theme->NotifyOnNativeThemeUpdated(); +} + +IN_PROC_BROWSER_TEST_F(DaoControlCenterPopupBrowserTest, + ForceDarkModeButtonLivesInMainUtilityRow) { + auto* theme = ui::NativeTheme::GetInstanceForNativeUi(); + theme->set_preferred_color_scheme( + ui::NativeTheme::PreferredColorScheme::kLight); + theme->NotifyOnNativeThemeUpdated(); + + auto* popup = GetBrowserView(browser())->dao_control_center_popup(); + ASSERT_NE(nullptr, popup); + popup->ShowAt(gfx::Point(100, 100)); + + const std::u16string label = + l10n_util::GetStringUTF16(IDS_DAO_FORCE_DARK_MODE_LABEL); + auto* utility_section = + FindDescendantViewOfClass(popup); + ASSERT_NE(nullptr, utility_section); + EXPECT_EQ((std::vector{ + u"QR Code", + l10n_util::GetStringUTF16(IDS_DAO_CONTROL_CENTER_MINI_DAO), + u"Security", + label, + u"More", + }), + GetDirectButtonAccessibleNames(utility_section)); + views::Button* button = FindButtonWithAccessibleName(popup, label); + ASSERT_NE(nullptr, button); + EXPECT_TRUE(button->IsDrawn()); + EXPECT_FALSE(button->GetEnabled()); + EXPECT_EQ(views::Button::STATE_DISABLED, button->GetState()); + EXPECT_EQ(l10n_util::GetStringUTF16( + IDS_DAO_FORCE_DARK_MODE_DISABLED_TOOLTIP), + button->GetTooltipText()); + auto* button_label = FindDescendantLabelWithText( + button, l10n_util::GetStringUTF16( + IDS_DAO_FORCE_DARK_MODE_SHORT_LABEL)); + ASSERT_NE(nullptr, button_label); + const SkColor disabled_label_color = button_label->GetEnabledColor(); + const int disabled_label_red = static_cast( + SkColorGetR(disabled_label_color)); + EXPECT_NE(ControlCenterLabelColor(), disabled_label_color); + EXPECT_NEAR(SkColorGetR(disabled_label_color), + SkColorGetG(disabled_label_color), 2); + EXPECT_NEAR(SkColorGetG(disabled_label_color), + SkColorGetB(disabled_label_color), 2); + EXPECT_GT(disabled_label_red, + static_cast(SkColorGetR(ControlCenterLabelColor()))); + EXPECT_LT(disabled_label_red, 200); + EXPECT_EQ(nullptr, FindDescendantViewOfClass(popup)); + + popup->Hide(); +} + +IN_PROC_BROWSER_TEST_F(DaoControlCenterPopupBrowserTest, + MoreMenuContainsShareAction) { + auto* popup = GetBrowserView(browser())->dao_control_center_popup(); + ASSERT_NE(nullptr, popup); + popup->ShowAt(gfx::Point(100, 100)); + popup->ShowMoreMenu(); + + auto* more_menu = FindDescendantViewOfClass(popup); + ASSERT_NE(nullptr, more_menu); + ASSERT_TRUE(more_menu->GetVisible()); + views::LabelButton* share_button = FindLabelButtonWithText( + more_menu, l10n_util::GetStringUTF16(IDS_DAO_CONTROL_CENTER_SHARE)); + ASSERT_NE(nullptr, share_button); + EXPECT_TRUE(share_button->IsDrawn()); + + popup->Hide(); +} + +IN_PROC_BROWSER_TEST_F(DaoControlCenterPopupBrowserTest, + ForceDarkModeUtilityButtonTogglesPreference) { + auto* theme = ui::NativeTheme::GetInstanceForNativeUi(); + theme->set_preferred_color_scheme( + ui::NativeTheme::PreferredColorScheme::kDark); + theme->NotifyOnNativeThemeUpdated(); + + PrefService* prefs = browser()->profile()->GetPrefs(); + ASSERT_TRUE(prefs); + prefs->SetBoolean(prefs::kDaoForceDarkModeEnabled, false); + + auto* popup = GetBrowserView(browser())->dao_control_center_popup(); + ASSERT_NE(nullptr, popup); + popup->ShowAt(gfx::Point(100, 100)); + + views::Button* button = FindButtonWithAccessibleName( + popup, l10n_util::GetStringUTF16(IDS_DAO_FORCE_DARK_MODE_LABEL)); + ASSERT_NE(nullptr, button); + ASSERT_TRUE(button->GetEnabled()); + + views::test::ButtonTestApi(button).NotifyClick(ui::MouseEvent( + ui::EventType::kMousePressed, gfx::Point(), gfx::Point(), + ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, + ui::EF_LEFT_MOUSE_BUTTON)); + EXPECT_TRUE(prefs->GetBoolean(prefs::kDaoForceDarkModeEnabled)); + EXPECT_TRUE(IsForceDarkModeEffective(browser()->profile())); + + views::test::ButtonTestApi(button).NotifyClick(ui::MouseEvent( + ui::EventType::kMousePressed, gfx::Point(), gfx::Point(), + ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, + ui::EF_LEFT_MOUSE_BUTTON)); + EXPECT_FALSE(prefs->GetBoolean(prefs::kDaoForceDarkModeEnabled)); + + popup->Hide(); +} + IN_PROC_BROWSER_TEST_F(DaoControlCenterPopupBrowserTest, PopupExists) { auto* popup = GetBrowserView(browser())->dao_control_center_popup(); ASSERT_NE(nullptr, popup); @@ -5465,6 +5641,37 @@ IN_PROC_BROWSER_TEST_F(DaoLittleDaoViewBrowserTest, removed.Wait(); } +IN_PROC_BROWSER_TEST_F(DaoLittleDaoViewBrowserTest, + SiteCenterDoesNotContainForceDarkModeControl) { + auto* theme = ui::NativeTheme::GetInstanceForNativeUi(); + theme->set_preferred_color_scheme( + ui::NativeTheme::PreferredColorScheme::kLight); + theme->NotifyOnNativeThemeUpdated(); + + Browser* little_dao_browser = dao::DaoLittleDaoController::OpenInLittleDao( + browser()->profile(), GURL("data:text/html,site-center-force-dark")); + ASSERT_NE(nullptr, little_dao_browser); + + BrowserView* little_browser_view = GetBrowserView(little_dao_browser); + ASSERT_NE(nullptr, little_browser_view); + little_browser_view->DeprecatedLayoutImmediately(); + + auto* little_view = little_browser_view->dao_little_dao_view(); + ASSERT_NE(nullptr, little_view); + auto* popup = little_browser_view->dao_mini_dao_site_center_popup(); + ASSERT_NE(nullptr, popup); + + little_view->ShowMiniDaoSiteCenterForTesting(); + EXPECT_EQ(nullptr, FindButtonWithAccessibleName( + popup, l10n_util::GetStringUTF16( + IDS_DAO_FORCE_DARK_MODE_LABEL))); + EXPECT_EQ(nullptr, FindDescendantViewOfClass(popup)); + + BrowserRemovedWaiter removed(little_dao_browser); + little_dao_browser->window()->Close(); + removed.Wait(); +} + IN_PROC_BROWSER_TEST_F(DaoLittleDaoViewBrowserTest, SiteCenterHideClearsHoveredActionBackground) { Browser* little_dao_browser = dao::DaoLittleDaoController::OpenInLittleDao( diff --git a/src/dao/browser/ui/views/dao_control_center_more_menu.cc b/src/dao/browser/ui/views/dao_control_center_more_menu.cc index 0272648a..dc94a0ea 100644 --- a/src/dao/browser/ui/views/dao_control_center_more_menu.cc +++ b/src/dao/browser/ui/views/dao_control_center_more_menu.cc @@ -80,6 +80,16 @@ DaoControlCenterMoreMenu::DaoControlCenterMoreMenu( back_btn->SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(4, 12))); AddChildView(std::move(back_btn)); + // Share button + { + auto btn = std::make_unique( + l10n_util::GetStringUTF16(IDS_DAO_CONTROL_CENTER_SHARE), + base::BindRepeating(&DaoControlCenterMoreMenu::OnShareClicked, + base::Unretained(this))); + share_button_ = btn.get(); + AddChildView(static_cast(btn.release())); + } + // Clear Cache button { auto btn = std::make_unique( @@ -109,6 +119,13 @@ void DaoControlCenterMoreMenu::OnBackClicked() { } } +void DaoControlCenterMoreMenu::OnShareClicked() { + if (popup_) { + popup_->ShareCurrentPage(share_button_ ? share_button_->GetBoundsInScreen() + : GetBoundsInScreen()); + } +} + void DaoControlCenterMoreMenu::OnClearCacheClicked() { if (!popup_ || !popup_->browser()) { return; diff --git a/src/dao/browser/ui/views/dao_control_center_more_menu.h b/src/dao/browser/ui/views/dao_control_center_more_menu.h index 3e8fee7b..d93a9a4a 100644 --- a/src/dao/browser/ui/views/dao_control_center_more_menu.h +++ b/src/dao/browser/ui/views/dao_control_center_more_menu.h @@ -17,7 +17,7 @@ namespace dao { class DaoControlCenterPopup; -// Sub-panel with additional actions: clear cache, clear cookies. +// Sub-panel with additional actions: share, clear cache, clear cookies. class DaoControlCenterMoreMenu : public views::View { METADATA_HEADER(DaoControlCenterMoreMenu, views::View) @@ -30,10 +30,12 @@ class DaoControlCenterMoreMenu : public views::View { private: void OnBackClicked(); + void OnShareClicked(); void OnClearCacheClicked(); void OnClearCookiesClicked(); raw_ptr popup_; + raw_ptr share_button_ = nullptr; raw_ptr clear_cache_button_ = nullptr; raw_ptr clear_cookies_button_ = nullptr; diff --git a/src/dao/browser/ui/views/dao_control_center_popup.cc b/src/dao/browser/ui/views/dao_control_center_popup.cc index 1d1e84d5..8aed3b97 100644 --- a/src/dao/browser/ui/views/dao_control_center_popup.cc +++ b/src/dao/browser/ui/views/dao_control_center_popup.cc @@ -4,7 +4,7 @@ #include "dao/browser/ui/views/dao_control_center_popup.h" -#include "third_party/blink/public/common/input/web_input_event.h" +#include "build/build_config.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "content/public/browser/web_contents.h" @@ -25,6 +25,15 @@ #include "ui/views/layout/box_layout.h" #include "ui/views/view_utils.h" +#if BUILDFLAG(IS_MAC) +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "dao/browser/ui/views/dao_native_share_mac.h" +#include "ui/views/widget/widget.h" +#endif + +#include "third_party/blink/public/common/input/web_input_event.h" + namespace dao { namespace { @@ -147,6 +156,9 @@ void DaoControlCenterPopup::ApplyTheme() { separator_->SetBackground( views::CreateSolidBackground(SeparatorColor())); } + if (utility_section_) { + utility_section_->Refresh(); + } } void DaoControlCenterPopup::OnNativeThemeUpdated( @@ -242,10 +254,40 @@ void DaoControlCenterPopup::ShowMainPanel() { if (card_->children().size() > 1) { card_->children()[1]->SetVisible(true); } + if (utility_section_) { + utility_section_->Refresh(); + } InvalidateLayout(); SchedulePaint(); } +void DaoControlCenterPopup::ShareCurrentPage(const gfx::Rect& anchor_rect) { +#if BUILDFLAG(IS_MAC) + if (!browser_) { + return; + } + auto* web_contents = browser_->tab_strip_model()->GetActiveWebContents(); + if (!web_contents) { + return; + } + + std::string url = web_contents->GetVisibleURL().spec(); + std::string title = web_contents->GetTitle().empty() + ? url + : base::UTF16ToUTF8(web_contents->GetTitle()); + + BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); + if (!browser_view || !browser_view->GetWidget()) { + return; + } + + dao::ShowNativeShareMac(url, title, browser_view->GetWidget()->GetNativeView(), + anchor_rect); +#else + (void)anchor_rect; +#endif +} + void DaoControlCenterPopup::OnTabStripModelChanged( TabStripModel* tab_strip_model, const TabStripModelChange& change, diff --git a/src/dao/browser/ui/views/dao_control_center_popup.h b/src/dao/browser/ui/views/dao_control_center_popup.h index 0582f4e8..391a9e69 100644 --- a/src/dao/browser/ui/views/dao_control_center_popup.h +++ b/src/dao/browser/ui/views/dao_control_center_popup.h @@ -16,6 +16,10 @@ class Browser; +namespace gfx { +class Rect; +} + namespace dao { class DaoControlCenterExtensionsSection; @@ -47,6 +51,8 @@ class DaoControlCenterPopup : public views::View, void ShowMoreMenu(); // Return to the main panel from a sub-panel. void ShowMainPanel(); + // Show the native share picker for the active page. + void ShareCurrentPage(const gfx::Rect& anchor_rect); Browser* browser() const { return browser_; } diff --git a/src/dao/browser/ui/views/dao_control_center_utility_section.cc b/src/dao/browser/ui/views/dao_control_center_utility_section.cc index ed80534f..e82750d4 100644 --- a/src/dao/browser/ui/views/dao_control_center_utility_section.cc +++ b/src/dao/browser/ui/views/dao_control_center_utility_section.cc @@ -6,6 +6,8 @@ #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" +#include "cc/paint/paint_flags.h" +#include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" @@ -14,6 +16,7 @@ #include "chrome/browser/ui/views/page_info/page_info_bubble_view.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/web_contents.h" +#include "dao/browser/dao_pref_names.h" #include "dao/browser/strings/grit/dao_strings.h" #include "dao/browser/ui/views/dao_colors.h" #include "dao/browser/ui/views/dao_control_center_popup.h" @@ -24,6 +27,9 @@ #include "ui/base/metadata/metadata_impl_macros.h" #include "ui/gfx/canvas.h" #include "ui/gfx/geometry/rect_f.h" +#include "third_party/skia/include/core/SkMatrix.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/utils/SkParsePath.h" #include "ui/views/background.h" #include "ui/views/bubble/bubble_border.h" #include "ui/views/bubble/bubble_dialog_delegate_view.h" @@ -33,19 +39,37 @@ #include "ui/views/widget/widget.h" #include "url/gurl.h" -#if BUILDFLAG(IS_MAC) -#include "dao/browser/ui/views/dao_native_share_mac.h" -#endif - namespace dao { namespace { -constexpr int kUtilButtonSize = 56; +constexpr int kUtilButtonSize = 48; constexpr int kUtilIconSize = 18; constexpr int kUtilCornerRadius = 10; constexpr int kUtilFontSize = 10; +void DrawFilledMoonIcon(gfx::Canvas* canvas, + const gfx::RectF& rect, + SkColor color) { + const float scale = rect.width() / 24.0f; + SkPath path; + SkParsePath::FromSVGString( + "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46." + "402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803." + "401", + &path); + SkMatrix matrix; + matrix.setScale(scale, scale); + matrix.postTranslate(rect.x(), rect.y()); + path = path.makeTransform(matrix); + + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setStyle(cc::PaintFlags::kFill_Style); + flags.setColor(color); + canvas->DrawPath(path, flags); +} + // A utility button with a Lucide icon and a text label. class UtilityButton : public views::Button { public: @@ -66,29 +90,49 @@ class UtilityButton : public views::Button { icon_spacer->SetCanProcessEventsWithinSubtree(false); AddChildView(icon_spacer.release()); - auto* label = - AddChildView(std::make_unique(label_text)); - label->SetFontList(gfx::FontList({"system-ui"}, gfx::Font::NORMAL, - kUtilFontSize, - gfx::Font::Weight::NORMAL)); - label->SetEnabledColor(ControlCenterLabelColor()); - label->SetCanProcessEventsWithinSubtree(false); + label_ = AddChildView(std::make_unique(label_text)); + label_->SetFontList(gfx::FontList({"system-ui"}, gfx::Font::NORMAL, + kUtilFontSize, + gfx::Font::Weight::NORMAL)); + label_->SetEnabledColor(ControlCenterLabelColor()); + label_->SetCanProcessEventsWithinSubtree(false); SetPreferredSize(gfx::Size(kUtilButtonSize, kUtilButtonSize)); SetInstallFocusRingOnFocus(false); SetAccessibleName(label_text); } + void SetSelected(bool selected) { + if (selected_ == selected) { + return; + } + selected_ = selected; + RefreshBackground(); + SchedulePaint(); + } + + void SetVisualEnabled(bool enabled) { + SetEnabled(enabled); + label_->SetEnabled(enabled); + label_->SetEnabledColor(enabled ? ControlCenterLabelColor() + : ControlCenterSecondaryTextColor()); + RefreshBackground(); + SchedulePaint(); + } + void OnMouseEntered(const ui::MouseEvent& event) override { Button::OnMouseEntered(event); - SetBackground(views::CreateRoundedRectBackground( - SuggestionHover(), kUtilCornerRadius)); - SchedulePaint(); + if (GetEnabled()) { + hovered_ = true; + RefreshBackground(); + SchedulePaint(); + } } void OnMouseExited(const ui::MouseEvent& event) override { Button::OnMouseExited(event); - SetBackground(nullptr); + hovered_ = false; + RefreshBackground(); SchedulePaint(); } @@ -96,15 +140,43 @@ class UtilityButton : public views::Button { // Draw the icon centered in the spacer area (first child). if (!children().empty()) { gfx::Rect icon_bounds = children().front()->bounds(); - DrawLucideIcon(canvas, icon_, - gfx::RectF(icon_bounds.x(), icon_bounds.y(), - kUtilIconSize, kUtilIconSize), - ControlCenterIconMuted()); + const gfx::RectF icon_rect(icon_bounds.x(), icon_bounds.y(), + kUtilIconSize, kUtilIconSize); + const SkColor icon_color = + GetEnabled() ? (selected_ ? ControlCenterIconDefault() + : ControlCenterIconMuted()) + : ControlCenterSecondaryTextColor(); + if (selected_ && icon_ == LucideIcon::kMoon) { + DrawFilledMoonIcon(canvas, icon_rect, icon_color); + } else { + DrawLucideIcon(canvas, icon_, icon_rect, icon_color); + } } } private: + void RefreshBackground() { + if (!GetEnabled()) { + SetBackground(nullptr); + return; + } + if (hovered_) { + SetBackground(views::CreateRoundedRectBackground( + SuggestionHover(), kUtilCornerRadius)); + return; + } + if (selected_) { + SetBackground(views::CreateRoundedRectBackground( + ControlCenterActiveBg(), kUtilCornerRadius)); + return; + } + SetBackground(nullptr); + } + LucideIcon icon_; + raw_ptr label_ = nullptr; + bool selected_ = false; + bool hovered_ = false; }; } // namespace @@ -120,14 +192,6 @@ DaoControlCenterUtilitySection::DaoControlCenterUtilitySection( layout->set_main_axis_alignment( views::BoxLayout::MainAxisAlignment::kCenter); - AddChildView(static_cast( - std::make_unique( - u"Share", LucideIcon::kShare, - base::BindRepeating( - &DaoControlCenterUtilitySection::OnShareClicked, - base::Unretained(this))) - .release())); - AddChildView(static_cast( std::make_unique( u"QR Code", LucideIcon::kQrCode, @@ -153,6 +217,19 @@ DaoControlCenterUtilitySection::DaoControlCenterUtilitySection( base::Unretained(this))) .release())); + { + auto button = std::make_unique( + l10n_util::GetStringUTF16(IDS_DAO_FORCE_DARK_MODE_SHORT_LABEL), + LucideIcon::kMoon, + base::BindRepeating( + &DaoControlCenterUtilitySection::OnForceDarkModeClicked, + base::Unretained(this))); + button->SetAccessibleName( + l10n_util::GetStringUTF16(IDS_DAO_FORCE_DARK_MODE_LABEL)); + force_dark_mode_button_ = button.get(); + AddChildView(static_cast(button.release())); + } + AddChildView(static_cast( std::make_unique( u"More", LucideIcon::kEllipsis, @@ -160,34 +237,14 @@ DaoControlCenterUtilitySection::DaoControlCenterUtilitySection( &DaoControlCenterUtilitySection::OnMoreClicked, base::Unretained(this))) .release())); + + RefreshForceDarkModeState(); } DaoControlCenterUtilitySection::~DaoControlCenterUtilitySection() = default; -void DaoControlCenterUtilitySection::OnShareClicked() { -#if BUILDFLAG(IS_MAC) - if (!popup_ || !popup_->browser()) { - return; - } - auto* web_contents = - popup_->browser()->tab_strip_model()->GetActiveWebContents(); - if (!web_contents) { - return; - } - std::string url = web_contents->GetVisibleURL().spec(); - std::string title = web_contents->GetTitle().empty() - ? url - : base::UTF16ToUTF8(web_contents->GetTitle()); - - BrowserView* browser_view = - BrowserView::GetBrowserViewForBrowser(popup_->browser()); - if (!browser_view || !browser_view->GetWidget()) { - return; - } - gfx::NativeView native_view = browser_view->GetWidget()->GetNativeView(); - gfx::Rect anchor_rect = GetBoundsInScreen(); - dao::ShowNativeShareMac(url, title, native_view, anchor_rect); -#endif +void DaoControlCenterUtilitySection::Refresh() { + RefreshForceDarkModeState(); } void DaoControlCenterUtilitySection::OnQrClicked() { @@ -219,6 +276,33 @@ void DaoControlCenterUtilitySection::OnMiniDaoClicked() { IDS_DAO_CONTROL_CENTER_MINI_DAO_FAILED_TOAST)); } +void DaoControlCenterUtilitySection::OnForceDarkModeClicked() { + if (!popup_ || !popup_->browser() || !IsForceDarkModeAvailable()) { + RefreshForceDarkModeState(); + return; + } + + Profile* profile = popup_->browser()->profile(); + SetForceDarkModeUserEnabled(profile, !IsForceDarkModeUserEnabled(profile)); + RefreshForceDarkModeState(); +} + +void DaoControlCenterUtilitySection::RefreshForceDarkModeState() { + if (!force_dark_mode_button_) { + return; + } + + auto* button = static_cast(force_dark_mode_button_.get()); + Profile* profile = + popup_ && popup_->browser() ? popup_->browser()->profile() : nullptr; + const bool available = IsForceDarkModeAvailable(); + button->SetSelected(IsForceDarkModeUserEnabled(profile)); + button->SetVisualEnabled(available); + button->SetTooltipText(l10n_util::GetStringUTF16( + available ? IDS_DAO_FORCE_DARK_MODE_TOOLTIP + : IDS_DAO_FORCE_DARK_MODE_DISABLED_TOOLTIP)); +} + void DaoControlCenterUtilitySection::OnLockClicked() { if (!popup_ || !popup_->browser()) { return; diff --git a/src/dao/browser/ui/views/dao_control_center_utility_section.h b/src/dao/browser/ui/views/dao_control_center_utility_section.h index a9a8d18c..c5d3be77 100644 --- a/src/dao/browser/ui/views/dao_control_center_utility_section.h +++ b/src/dao/browser/ui/views/dao_control_center_utility_section.h @@ -8,11 +8,15 @@ #include "base/memory/raw_ptr.h" #include "ui/views/view.h" +namespace views { +class Button; +} // namespace views + namespace dao { class DaoControlCenterPopup; -// A row of utility buttons: Share, QR Code, Mini Dao, Lock (Page Info), More. +// A row of utility buttons: QR Code, Mini Dao, Security, dark mode, More. class DaoControlCenterUtilitySection : public views::View { METADATA_HEADER(DaoControlCenterUtilitySection, views::View) @@ -24,14 +28,18 @@ class DaoControlCenterUtilitySection : public views::View { const DaoControlCenterUtilitySection&) = delete; ~DaoControlCenterUtilitySection() override; + void Refresh(); + private: - void OnShareClicked(); void OnQrClicked(); void OnMiniDaoClicked(); + void OnForceDarkModeClicked(); + void RefreshForceDarkModeState(); void OnLockClicked(); void OnMoreClicked(); raw_ptr popup_; + raw_ptr force_dark_mode_button_ = nullptr; }; } // namespace dao diff --git a/src/dao/browser/ui/views/dao_lucide_icons.cc b/src/dao/browser/ui/views/dao_lucide_icons.cc index f7b9e186..f30fa05f 100644 --- a/src/dao/browser/ui/views/dao_lucide_icons.cc +++ b/src/dao/browser/ui/views/dao_lucide_icons.cc @@ -348,6 +348,20 @@ void DrawSparkles(gfx::Canvas* canvas, canvas->DrawCircle(gfx::PointF(ox + 4 * s, oy + 20 * s), 2.0f * s, flags); } +// Lucide "moon" +void DrawMoon(gfx::Canvas* canvas, + float s, + float ox, + float oy, + const cc::PaintFlags& flags) { + DrawSvgPath( + canvas, + "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46." + "402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803." + "401", + s, ox, oy, flags); +} + } // namespace void DrawLucideIcon(gfx::Canvas* canvas, @@ -445,6 +459,12 @@ void DrawLucideIcon(gfx::Canvas* canvas, case LucideIcon::kSparkles: DrawSparkles(canvas, s, ox, oy, flags); break; + case LucideIcon::kMoon: { + cc::PaintFlags moon_flags = flags; + moon_flags.setStrokeWidth(2.0f * s); + DrawMoon(canvas, s, ox, oy, moon_flags); + break; + } } } diff --git a/src/dao/browser/ui/views/dao_lucide_icons.h b/src/dao/browser/ui/views/dao_lucide_icons.h index 61d04bfe..7938936f 100644 --- a/src/dao/browser/ui/views/dao_lucide_icons.h +++ b/src/dao/browser/ui/views/dao_lucide_icons.h @@ -43,6 +43,7 @@ enum class LucideIcon { kSquareArrowDownLeft, kBot, kSparkles, + kMoon, }; // Draw a Lucide icon into |rect| on |canvas| using |color|. diff --git a/src/patches/chrome/browser/chrome_content_browser_client_force_dark_mode.cc.patch b/src/patches/chrome/browser/chrome_content_browser_client_force_dark_mode.cc.patch new file mode 100644 index 00000000..0670620c --- /dev/null +++ b/src/patches/chrome/browser/chrome_content_browser_client_force_dark_mode.cc.patch @@ -0,0 +1,54 @@ +diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc +index 0000000000..0000000000 100644 +--- a/chrome/browser/chrome_content_browser_client.cc ++++ b/chrome/browser/chrome_content_browser_client.cc +@@ -355,6 +355,7 @@ + #include "content/public/common/content_switches.h" + #include "content/public/common/origin_util.h" + #include "content/public/common/url_utils.h" ++#include "dao/browser/dao_pref_names.h" + #include "content/public/common/window_container_type.mojom-shared.h" + #include "device/vr/buildflags/buildflags.h" + #include "extensions/browser/browser_frame_context_data.h" +@@ -4851,6 +4852,8 @@ void ChromeContentBrowserClient::OverrideWebPreferences( + web_prefs->root_scrollbar_theme_color = + GetRootScrollbarThemeColor(web_contents); + ++ dao::ApplyForceDarkModePreferences(profile, web_prefs); ++ + web_prefs->translate_service_available = TranslateService::IsAvailable(prefs); + + std::optional style = +@@ -4977,6 +4980,21 @@ bool ChromeContentBrowserClient::OverrideWebPreferencesAfterNavigation( + } + #endif + ++ const bool old_force_dark_mode_enabled = ++ web_prefs->force_dark_mode_enabled; ++ const auto old_dao_preferred_color_scheme = ++ web_prefs->preferred_color_scheme; ++ const auto old_dao_preferred_root_scrollbar_color_scheme = ++ web_prefs->preferred_root_scrollbar_color_scheme; ++ dao::ApplyForceDarkModePreferences( ++ Profile::FromBrowserContext(web_contents->GetBrowserContext()), ++ web_prefs); ++ prefs_changed |= ++ web_prefs->force_dark_mode_enabled != old_force_dark_mode_enabled || ++ web_prefs->preferred_color_scheme != old_dao_preferred_color_scheme || ++ web_prefs->preferred_root_scrollbar_color_scheme != ++ old_dao_preferred_root_scrollbar_color_scheme; ++ + #if BUILDFLAG(IS_CHROMEOS) + const bool subapps_apis_require_user_gesture_and_authorization = + SubAppsAPIsRequireUserGestureAndAuthorization(web_contents); +@@ -5006,6 +5024,9 @@ bool ChromeContentBrowserClient:: + GetPreferredColorScheme(prefs, main_frame_site.GetSiteURL(), + &web_contents) != + std::tie(prefs.preferred_color_scheme, + prefs.preferred_root_scrollbar_color_scheme) || ++ dao::IsForceDarkModeEffective(Profile::FromBrowserContext( ++ web_contents.GetBrowserContext())) != ++ prefs.force_dark_mode_enabled || + GetRootScrollbarThemeColor(&web_contents) != + prefs.root_scrollbar_theme_color; + } diff --git a/website/components/FeatureGrid.module.css b/website/components/FeatureGrid.module.css index 38ba40c0..e279a56e 100644 --- a/website/components/FeatureGrid.module.css +++ b/website/components/FeatureGrid.module.css @@ -48,6 +48,16 @@ background: var(--surface-hover); } +.highlight { + background: var(--accent-subtle); + border-color: var(--accent-dim); + box-shadow: inset 0 0 0 1px var(--accent-dim); +} + +.highlight:hover { + background: var(--accent-subtle); +} + .icon { color: var(--accent); } @@ -65,3 +75,15 @@ color: var(--text-secondary); margin: 0; } + +@media (min-width: 641px) { + .highlight { + grid-column: span 2; + } +} + +@media (max-width: 640px) { + .highlight { + grid-column: auto; + } +} diff --git a/website/components/FeatureGrid.tsx b/website/components/FeatureGrid.tsx index 269e27d6..c6c10e29 100644 --- a/website/components/FeatureGrid.tsx +++ b/website/components/FeatureGrid.tsx @@ -5,9 +5,16 @@ interface GridItem { icon: IconName; title: string; body: string; + highlight?: boolean; } const ITEMS: GridItem[] = [ + { + icon: 'moon', + title: 'Force Dark Mode', + body: 'Darken bright sites with Chromium Auto Dark Mode, only when your system is already dark.', + highlight: true, + }, { icon: 'picture-in-picture-2', title: 'Picture-in-Picture', @@ -46,7 +53,7 @@ export function FeatureGrid() {

And there's more.

    {ITEMS.map((it) => ( -
  • +
  • {it.title}

    {it.body}

    diff --git a/website/components/ui/LucideIcon.tsx b/website/components/ui/LucideIcon.tsx index 06dd0e6b..113ae636 100644 --- a/website/components/ui/LucideIcon.tsx +++ b/website/components/ui/LucideIcon.tsx @@ -25,6 +25,7 @@ export type IconName = | 'sliders-horizontal' | 'square' | 'palette' + | 'moon' | 'globe' | 'menu' | 'x' @@ -151,6 +152,11 @@ const ICON_PATHS: Record = { ), + moon: ( + <> + + + ), globe: ( <> diff --git a/website/lib/__tests__/homepage-features.test.ts b/website/lib/__tests__/homepage-features.test.ts new file mode 100644 index 00000000..95fa15c5 --- /dev/null +++ b/website/lib/__tests__/homepage-features.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const featureGridSource = readFileSync( + resolve(dirname(fileURLToPath(import.meta.url)), '../../components/FeatureGrid.tsx'), + 'utf8', +); + +describe('homepage feature grid', () => { + it('highlights Force Dark Mode as a feature', () => { + expect(featureGridSource).toContain('Force Dark Mode'); + expect(featureGridSource).toContain('Chromium Auto Dark Mode'); + expect(featureGridSource).toContain("icon: 'moon'"); + }); +});