blob: 5a686d5853908dc0d2df1c43739710d442dfbd1b [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/dark_mode_manager_linux.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/ui_features.h"
#include "components/dbus/thread_linux/dbus_thread_linux.h"
#include "components/dbus/xdg/systemd.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "ui/linux/linux_ui.h"
#include "ui/linux/linux_ui_factory.h"
#include "ui/native_theme/native_theme.h"
namespace ui {
DarkModeManagerLinux::DarkModeManagerLinux()
: DarkModeManagerLinux(
dbus_thread_linux::GetSharedSessionBus(),
ui::GetDefaultLinuxUiTheme(),
&ui::GetLinuxUiThemes(),
std::vector<raw_ptr<ui::NativeTheme, VectorExperimental>>{
ui::NativeTheme::GetInstanceForNativeUi(),
ui::NativeTheme::GetInstanceForWeb()}) {}
DarkModeManagerLinux::DarkModeManagerLinux(
scoped_refptr<dbus::Bus> bus,
LinuxUiTheme* default_linux_ui_theme,
const std::vector<raw_ptr<LinuxUiTheme, VectorExperimental>>*
linux_ui_themes,
std::vector<raw_ptr<NativeTheme, VectorExperimental>> native_themes)
: linux_ui_themes_(linux_ui_themes),
native_themes_(native_themes),
bus_(bus),
settings_proxy_(bus_->GetObjectProxy(
kFreedesktopSettingsService,
dbus::ObjectPath(kFreedesktopSettingsObjectPath))) {
dbus_xdg::SetSystemdScopeUnitNameForXdgPortal(
bus_.get(), base::BindOnce(&DarkModeManagerLinux::OnSystemdUnitStarted,
weak_ptr_factory_.GetWeakPtr()));
// Read the toolkit preference while asynchronously fetching the
// portal preference.
if (default_linux_ui_theme) {
auto* native_theme = default_linux_ui_theme->GetNativeTheme();
native_theme_observer_.Observe(native_theme);
SetColorScheme(native_theme->ShouldUseDarkColors(), true);
}
}
DarkModeManagerLinux::~DarkModeManagerLinux() = default;
void DarkModeManagerLinux::OnNativeThemeUpdated(
ui::NativeTheme* observed_theme) {
SetColorScheme(observed_theme->ShouldUseDarkColors(), true);
}
void DarkModeManagerLinux::OnSystemdUnitStarted(dbus_xdg::SystemdUnitStatus) {
// Subscribe to changes in the color scheme preference.
settings_proxy_->ConnectToSignal(
kFreedesktopSettingsInterface, kSettingChangedSignal,
base::BindRepeating(&DarkModeManagerLinux::OnPortalSettingChanged,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&DarkModeManagerLinux::OnSignalConnected,
weak_ptr_factory_.GetWeakPtr()));
// Read initial color scheme preference.
{
dbus::MethodCall method_call(kFreedesktopSettingsInterface, kReadMethod);
dbus::MessageWriter writer(&method_call);
writer.AppendString(kSettingsNamespace);
writer.AppendString(kColorSchemeKey);
settings_proxy_->CallMethodWithErrorResponse(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&DarkModeManagerLinux::OnReadColorScheme,
weak_ptr_factory_.GetWeakPtr()));
}
// Read initial accent color preference.
if (base::FeatureList::IsEnabled(features::kUsePortalAccentColor)) {
dbus::MethodCall method_call(kFreedesktopSettingsInterface, kReadMethod);
dbus::MessageWriter writer(&method_call);
writer.AppendString(kSettingsNamespace);
writer.AppendString(kAccentColorKey);
settings_proxy_->CallMethodWithErrorResponse(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&DarkModeManagerLinux::OnReadAccentColor,
weak_ptr_factory_.GetWeakPtr()));
}
}
void DarkModeManagerLinux::OnSignalConnected(const std::string& interface_name,
const std::string& signal_name,
bool connected) {
// Nothing to do. Continue using the toolkit setting if !connected.
}
void DarkModeManagerLinux::OnPortalSettingChanged(dbus::Signal* signal) {
dbus::MessageReader reader(signal);
std::string namespace_changed;
std::string key_changed;
dbus::MessageReader variant_reader(nullptr);
if (!reader.PopString(&namespace_changed) ||
!reader.PopString(&key_changed) || !reader.PopVariant(&variant_reader)) {
LOG(ERROR) << "Received malformed Setting Changed signal from "
"org.freedesktop.portal.Settings";
return;
}
if (namespace_changed != kSettingsNamespace) {
return;
}
if (key_changed == kColorSchemeKey) {
uint32_t new_color_scheme;
if (!variant_reader.PopUint32(&new_color_scheme)) {
LOG(ERROR)
<< "Failed to read color-scheme value from SettingChanged signal";
return;
}
SetColorScheme(new_color_scheme == kFreedesktopColorSchemeDark, false);
} else if (key_changed == kAccentColorKey &&
base::FeatureList::IsEnabled(features::kUsePortalAccentColor)) {
SetAccentColor(&variant_reader);
}
}
void DarkModeManagerLinux::OnReadColorScheme(
dbus::Response* response,
dbus::ErrorResponse* error_response) {
if (!response) {
// Continue using the toolkit setting.
return;
}
dbus::MessageReader reader(response);
dbus::MessageReader variant_reader(nullptr);
if (!reader.PopVariant(&variant_reader)) {
LOG(ERROR) << "Failed to read variant from Read method response";
return;
}
uint32_t new_color_scheme;
if (!variant_reader.PopVariantOfUint32(&new_color_scheme)) {
LOG(ERROR) << "Failed to read color-scheme value from Read "
"method response";
return;
}
// Once we read the org.freedesktop.appearance color-scheme setting
// successfully, it should always take precedence over the toolkit color
// scheme.
ignore_toolkit_theme_changes_ = true;
SetColorScheme(new_color_scheme == kFreedesktopColorSchemeDark, false);
}
void DarkModeManagerLinux::OnReadAccentColor(
dbus::Response* response,
dbus::ErrorResponse* error_response) {
if (!response) {
// Continue using the toolkit setting.
return;
}
dbus::MessageReader reader(response);
dbus::MessageReader outer_variant_reader(nullptr);
if (!reader.PopVariant(&outer_variant_reader)) {
LOG(ERROR) << "Failed to read variant from Read method response";
return;
}
dbus::MessageReader inner_variant_reader(nullptr);
if (!outer_variant_reader.PopVariant(&inner_variant_reader)) {
LOG(ERROR) << "Failed to read variant from Read method response";
return;
}
SetAccentColor(&inner_variant_reader);
}
void DarkModeManagerLinux::SetColorScheme(bool prefer_dark_theme,
bool from_toolkit_theme) {
if (from_toolkit_theme && ignore_toolkit_theme_changes_) {
return;
}
if (!from_toolkit_theme) {
for (ui::LinuxUiTheme* linux_ui_theme : *linux_ui_themes_) {
linux_ui_theme->SetDarkTheme(prefer_dark_theme);
}
}
if (prefer_dark_theme_ == prefer_dark_theme) {
return;
}
prefer_dark_theme_ = prefer_dark_theme;
for (NativeTheme* theme : native_themes_) {
theme->set_use_dark_colors(prefer_dark_theme_);
theme->set_preferred_color_scheme(
prefer_dark_theme_ ? NativeTheme::PreferredColorScheme::kDark
: NativeTheme::PreferredColorScheme::kLight);
theme->NotifyOnNativeThemeUpdated();
}
}
void DarkModeManagerLinux::SetAccentColor(dbus::MessageReader* reader) {
dbus::MessageReader struct_reader(nullptr);
if (!reader->PopStruct(&struct_reader)) {
LOG(ERROR) << "Failed to read struct";
return;
}
bool valid = true;
int color[3] = {0, 0, 0};
for (int& channel : color) {
double d;
if (!struct_reader.PopDouble(&d)) {
LOG(ERROR) << "Failed to read double";
return;
}
if (d >= 0.0 && d <= 1.0) {
channel = d * 255;
} else {
// The org.freedesktop.impl.portal.Settings spec requires out-of-range RGB
// values to be treated as unset.
valid = false;
}
}
std::optional<SkColor> accent_color;
if (valid) {
accent_color = SkColorSetRGB(color[0], color[1], color[2]);
}
for (ui::LinuxUiTheme* linux_ui_theme : *linux_ui_themes_) {
linux_ui_theme->SetAccentColor(accent_color);
}
for (NativeTheme* theme : native_themes_) {
theme->set_user_color(accent_color);
theme->NotifyOnNativeThemeUpdated();
}
}
} // namespace ui