blob: 04365d8b5bd5f6f95b9517e5aba9e8a2eb6ae6ae [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/x/x11_cursor_loader.h"
#include <dlfcn.h>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include "base/compiler_specific.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/numerics/byte_conversions.h"
#include "base/numerics/checked_math.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/x/atom_cache.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/xproto.h"
#if BUILDFLAG(IS_LINUX)
#include "ui/linux/linux_ui.h"
#endif
extern "C" {
const char* XcursorLibraryPath(void);
}
namespace ui {
namespace {
using ThemeAndCursorName = std::pair<std::string, std::string>;
// Track the addition of an object to a set, removing it automatically when
// the ScopedSetInsertion goes out of scope.
class ScopedSetInsertion {
public:
ScopedSetInsertion(base::flat_set<ThemeAndCursorName>* org_set,
const ThemeAndCursorName& elem)
: set_(org_set), elem_(elem) {
set_->insert(elem);
}
ScopedSetInsertion(const ScopedSetInsertion&) = delete;
ScopedSetInsertion& operator=(const ScopedSetInsertion&) = delete;
~ScopedSetInsertion() { set_->erase(elem_); }
private:
const raw_ptr<base::flat_set<ThemeAndCursorName>> set_;
const ThemeAndCursorName elem_;
};
std::string GetEnv(const std::string& var) {
return base::Environment::Create()->GetVar(var).value_or(std::string());
}
NO_SANITIZE("cfi-icall")
std::string CursorPathFromLibXcursor() {
struct DlCloser {
void operator()(void* ptr) const { dlclose(ptr); }
};
std::unique_ptr<void, DlCloser> lib(dlopen("libXcursor.so.1", RTLD_LAZY));
if (!lib)
return "";
if (auto* sym = reinterpret_cast<decltype(&XcursorLibraryPath)>(
dlsym(lib.get(), "XcursorLibraryPath"))) {
if (const char* path = sym())
return path;
}
return "";
}
std::string CursorPathImpl() {
constexpr const char kDefaultPath[] =
"~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps:"
"/usr/X11R6/lib/X11/icons";
auto libxcursor_path = CursorPathFromLibXcursor();
if (!libxcursor_path.empty())
return libxcursor_path;
std::string path = GetEnv("XCURSOR_PATH");
return path.empty() ? kDefaultPath : path;
}
const std::string& CursorPath() {
static base::NoDestructor<std::string> path(CursorPathImpl());
return *path;
}
x11::Render::PictFormat GetRenderARGBFormat(
const x11::Render::QueryPictFormatsReply& formats) {
for (const auto& format : formats.formats) {
if (format.type == x11::Render::PictType::Direct && format.depth == 32 &&
format.direct.alpha_shift == 24 && format.direct.alpha_mask == 0xff &&
format.direct.red_shift == 16 && format.direct.red_mask == 0xff &&
format.direct.green_shift == 8 && format.direct.green_mask == 0xff &&
format.direct.blue_shift == 0 && format.direct.blue_mask == 0xff) {
return format.id;
}
}
return {};
}
std::vector<std::string> GetBaseThemes(const base::FilePath& abspath) {
DCHECK(abspath.IsAbsolute());
constexpr const char kKeyInherits[] = "Inherits";
std::string contents;
base::ReadFileToString(abspath, &contents);
base::StringPairs pairs;
base::SplitStringIntoKeyValuePairs(contents, '=', '\n', &pairs);
for (const auto& pair : pairs) {
if (base::TrimWhitespaceASCII(pair.first, base::TRIM_ALL) == kKeyInherits) {
return base::SplitString(pair.second, ",;", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
}
return {};
}
base::FilePath CanonicalizePath(base::FilePath path) {
std::vector<std::string> components = path.GetComponents();
if (components[0] == "~") {
path = base::GetHomeDir();
for (size_t i = 1; i < components.size(); i++)
path = path.Append(components[i]);
} else {
path = base::MakeAbsoluteFilePath(path);
}
return path;
}
scoped_refptr<base::RefCountedMemory> ReadCursorFromThemeImpl(
const std::string& theme,
const std::string& cursor_name,
base::flat_set<ThemeAndCursorName>* parent_theme_and_cursor_names,
base::flat_map<ThemeAndCursorName, scoped_refptr<base::RefCountedMemory>>*
cache) {
constexpr const char kCursorDir[] = "cursors";
constexpr const char kThemeInfo[] = "index.theme";
auto theme_and_cursor_name = std::make_pair(theme, cursor_name);
auto it = cache->find(theme_and_cursor_name);
if (it != cache->end()) {
return it->second;
}
if (parent_theme_and_cursor_names->contains(theme_and_cursor_name)) {
// Circular dependency.
return nullptr;
}
ScopedSetInsertion scoped_set_insertion(parent_theme_and_cursor_names,
theme_and_cursor_name);
std::vector<std::string> base_themes;
auto paths = base::SplitString(CursorPath(), ":", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const auto& path : paths) {
auto dir = CanonicalizePath(base::FilePath(path));
if (dir.empty())
continue;
base::FilePath theme_dir = dir.Append(theme);
base::FilePath cursor_dir = theme_dir.Append(kCursorDir);
std::string contents;
if (base::ReadFileToString(cursor_dir.Append(cursor_name), &contents)) {
auto result =
base::MakeRefCounted<base::RefCountedString>(std::move(contents));
(*cache)[theme_and_cursor_name] = result;
return result;
}
if (base_themes.empty())
base_themes = GetBaseThemes(theme_dir.Append(kThemeInfo));
}
for (const auto& path : base_themes) {
if (auto contents = ReadCursorFromThemeImpl(
path, cursor_name, parent_theme_and_cursor_names, cache)) {
(*cache)[theme_and_cursor_name] = contents;
return contents;
}
}
(*cache)[theme_and_cursor_name] = nullptr;
return nullptr;
}
// Reads the cursor called `name` for the theme named `theme`. Searches all
// paths in the XCursor path and parent themes.
scoped_refptr<base::RefCountedMemory> ReadCursorFromTheme(
const std::string& theme,
const std::string& cursor_name) {
base::flat_set<ThemeAndCursorName> parent_theme_names;
base::flat_map<ThemeAndCursorName, scoped_refptr<base::RefCountedMemory>>
cache;
return ReadCursorFromThemeImpl(theme, cursor_name, &parent_theme_names,
&cache);
}
scoped_refptr<base::RefCountedMemory> ReadCursorFile(
const std::string& cursor_name,
const std::string& rm_xcursor_theme) {
constexpr const char kDefaultTheme[] = "default";
std::string themes[] = {
#if BUILDFLAG(IS_LINUX)
// The toolkit theme has the highest priority.
LinuxUi::instance() ? LinuxUi::instance()->GetCursorThemeName()
: std::string(),
#endif
// Next try Xcursor.theme.
rm_xcursor_theme,
// As a last resort, use the default theme.
kDefaultTheme,
};
for (const std::string& theme : themes) {
if (theme.empty())
continue;
if (auto file = ReadCursorFromTheme(theme, cursor_name)) {
return file;
}
}
return nullptr;
}
std::vector<XCursorLoader::Image> ReadCursorImages(
const std::vector<std::string>& cursor_names,
const std::string& rm_xcursor_theme,
uint32_t preferred_size) {
// Fallback on a left pointer if possible.
auto cursor_names_copy = cursor_names;
cursor_names_copy.push_back("left_ptr");
for (const auto& cursor_name : cursor_names_copy) {
if (auto contents = ReadCursorFile(cursor_name, rm_xcursor_theme)) {
auto images = ParseCursorFile(contents, preferred_size);
if (!images.empty())
return images;
}
}
return {};
}
} // namespace
XCursorLoader::XCursorLoader(x11::Connection* connection,
base::RepeatingClosure on_cursor_config_changed)
: connection_(connection),
on_cursor_config_changed_(std::move(on_cursor_config_changed)),
rm_cache_(connection,
connection->default_root(),
{x11::Atom::RESOURCE_MANAGER},
base::BindRepeating(&XCursorLoader::OnPropertyChanged,
base::Unretained(this))) {
auto pf_cookie = connection_->render().QueryPictFormats();
cursor_font_ = connection_->GenerateId<x11::Font>();
connection_->OpenFont({cursor_font_, "cursor"});
// Fetch the initial property value which will call `OnPropertyChanged` and
// initialize `rm_xcursor_theme_`, `rm_xcursor_size_`, and `rm_xft_dpi_`.
rm_cache_.Get(x11::Atom::RESOURCE_MANAGER);
if (auto pf_reply = pf_cookie.Sync())
pict_format_ = GetRenderARGBFormat(*pf_reply.reply);
}
XCursorLoader::~XCursorLoader() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
scoped_refptr<X11Cursor> XCursorLoader::LoadCursor(
const std::vector<std::string>& cursor_names) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto cursor = base::MakeRefCounted<X11Cursor>();
if (SupportsCreateCursor()) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(ReadCursorImages, cursor_names, rm_xcursor_theme_,
GetPreferredCursorSize()),
base::BindOnce(&XCursorLoader::LoadCursorImpl,
weak_factory_.GetWeakPtr(), cursor, cursor_names));
} else {
LoadCursorImpl(cursor, cursor_names, {});
}
return cursor;
}
scoped_refptr<X11Cursor> XCursorLoader::CreateCursor(
const std::vector<Image>& images) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<scoped_refptr<X11Cursor>> cursors;
std::vector<x11::Render::AnimationCursorElement> elements;
cursors.reserve(images.size());
elements.reserve(images.size());
for (const Image& image : images) {
auto cursor = CreateCursor(image.bitmap, image.hotspot);
cursors.push_back(cursor);
elements.push_back(x11::Render::AnimationCursorElement{
cursor->xcursor_,
static_cast<uint32_t>(image.frame_delay.InMilliseconds())});
}
if (elements.empty())
return nullptr;
if (elements.size() == 1 || !SupportsCreateAnimCursor())
return cursors[0];
auto cursor = connection_->GenerateId<x11::Cursor>();
connection_->render().CreateAnimCursor({cursor, elements});
return base::MakeRefCounted<X11Cursor>(cursor);
}
scoped_refptr<X11Cursor> XCursorLoader::CreateCursor(
const SkBitmap& bitmap,
const gfx::Point& hotspot) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto pixmap = connection_->GenerateId<x11::Pixmap>();
auto gc = connection_->GenerateId<x11::GraphicsContext>();
uint16_t width = bitmap.width();
uint16_t height = bitmap.height();
connection_->CreatePixmap(
{32, pixmap, connection_->default_root(), width, height});
connection_->CreateGC({gc, pixmap});
size_t size = bitmap.computeByteSize();
std::vector<uint8_t> vec(size);
UNSAFE_TODO(memcpy(vec.data(), bitmap.getPixels(), size));
auto* connection = x11::Connection::Get();
x11::PutImageRequest put_image_request{
.format = x11::ImageFormat::ZPixmap,
.drawable = pixmap,
.gc = gc,
.width = width,
.height = height,
.depth = 32,
.data = base::MakeRefCounted<base::RefCountedBytes>(std::move(vec)),
};
connection->PutImage(put_image_request);
x11::Render::Picture pic = connection_->GenerateId<x11::Render::Picture>();
connection_->render().CreatePicture({pic, pixmap, pict_format_});
auto cursor = connection_->GenerateId<x11::Cursor>();
connection_->render().CreateCursor({cursor, pic,
static_cast<uint16_t>(hotspot.x()),
static_cast<uint16_t>(hotspot.y())});
connection_->render().FreePicture({pic});
connection_->FreePixmap({pixmap});
connection_->FreeGC({gc});
return base::MakeRefCounted<X11Cursor>(cursor);
}
void XCursorLoader::LoadCursorImpl(
scoped_refptr<X11Cursor> cursor,
const std::vector<std::string>& cursor_names,
const std::vector<XCursorLoader::Image>& images) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto xcursor = connection_->GenerateId<x11::Cursor>();
if (!images.empty()) {
xcursor = CreateCursor(images)->ReleaseCursor();
} else {
// Fallback to using a font cursor.
auto core_char = CursorNamesToChar(cursor_names);
constexpr uint16_t kFontCursorFgColor = 0;
constexpr uint16_t kFontCursorBgColor = 65535;
connection_->CreateGlyphCursor({xcursor, cursor_font_, cursor_font_,
static_cast<uint16_t>(2 * core_char),
static_cast<uint16_t>(2 * core_char + 1),
kFontCursorFgColor, kFontCursorFgColor,
kFontCursorFgColor, kFontCursorBgColor,
kFontCursorBgColor, kFontCursorBgColor});
}
cursor->SetCursor(xcursor);
}
uint32_t XCursorLoader::GetPreferredCursorSize() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
constexpr const char kXcursorSizeEnv[] = "XCURSOR_SIZE";
constexpr unsigned int kCursorSizeInchNum = 16;
constexpr unsigned int kCursorSizeInchDen = 72;
constexpr unsigned int kScreenCursorRatio = 48;
// Allow the XCURSOR_SIZE environment variable to override GTK settings.
int size;
if (base::StringToInt(GetEnv(kXcursorSizeEnv), &size) && size > 0) {
return size;
}
#if BUILDFLAG(IS_LINUX)
// Let the toolkit have the next say.
auto* linux_ui = LinuxUi::instance();
size = linux_ui ? linux_ui->GetCursorThemeSize() : 0;
if (size > 0) {
return size;
}
#endif
// Use Xcursor.size from RESOURCE_MANAGER if available.
if (rm_xcursor_size_) {
return rm_xcursor_size_;
}
// Guess the cursor size based on the DPI.
if (rm_xft_dpi_)
return rm_xft_dpi_ * kCursorSizeInchNum / kCursorSizeInchDen;
// As a last resort, guess the cursor size based on the screen size.
const auto& screen = connection_->default_screen();
return std::min(screen.width_in_pixels, screen.height_in_pixels) /
kScreenCursorRatio;
}
void XCursorLoader::ParseXResources(std::string_view resources) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::StringPairs pairs;
base::SplitStringIntoKeyValuePairs(resources, ':', '\n', &pairs);
for (const auto& pair : pairs) {
auto key = base::TrimWhitespaceASCII(pair.first, base::TRIM_ALL);
auto value = base::TrimWhitespaceASCII(pair.second, base::TRIM_ALL);
if (key == "Xcursor.theme")
rm_xcursor_theme_ = std::string(value);
else if (key == "Xcursor.size")
base::StringToUint(value, &rm_xcursor_size_);
else if (key == "Xft.dpi")
base::StringToUint(value, &rm_xft_dpi_);
}
}
uint16_t XCursorLoader::CursorNamesToChar(
const std::vector<std::string>& cursor_names) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// These cursor names are indexed by their ID in a cursor font.
constexpr auto kMap = base::MakeFixedFlatMap<std::string_view, uint16_t>({
{"X_cursor", 0u},
{"arrow", 1u},
{"based_arrow_down", 2u},
{"based_arrow_up", 3u},
{"boat", 4u},
{"bogosity", 5u},
{"bottom_left_corner", 6u},
{"bottom_right_corner", 7u},
{"bottom_side", 8u},
{"bottom_tee", 9u},
{"box_spiral", 10u},
{"center_ptr", 11u},
{"circle", 12u},
{"clock", 13u},
{"coffee_mug", 14u},
{"cross", 15u},
{"cross_reverse", 16u},
{"crosshair", 17u},
{"diamond_cross", 18u},
{"dot", 19u},
{"dotbox", 20u},
{"double_arrow", 21u},
{"draft_large", 22u},
{"draft_small", 23u},
{"draped_box", 24u},
{"exchange", 25u},
{"fleur", 26u},
{"gobbler", 27u},
{"gumby", 28u},
{"hand1", 29u},
{"hand2", 30u},
{"heart", 31u},
{"icon", 32u},
{"iron_cross", 33u},
{"left_ptr", 34u},
{"left_side", 35u},
{"left_tee", 36u},
{"leftbutton", 37u},
{"ll_angle", 38u},
{"lr_angle", 39u},
{"man", 40u},
{"middlebutton", 41u},
{"mouse", 42u},
{"pencil", 43u},
{"pirate", 44u},
{"plus", 45u},
{"question_arrow", 46u},
{"right_ptr", 47u},
{"right_side", 48u},
{"right_tee", 49u},
{"rightbutton", 50u},
{"rtl_logo", 51u},
{"sailboat", 52u},
{"sb_down_arrow", 53u},
{"sb_h_double_arrow", 54u},
{"sb_left_arrow", 55u},
{"sb_right_arrow", 56u},
{"sb_up_arrow", 57u},
{"sb_v_double_arrow", 58u},
{"shuttle", 59u},
{"sizing", 60u},
{"spider", 61u},
{"spraycan", 62u},
{"star", 63u},
{"target", 64u},
{"tcross", 65u},
{"top_left_arrow", 66u},
{"top_left_corner", 67u},
{"top_right_corner", 68u},
{"top_side", 69u},
{"top_tee", 70u},
{"trek", 71u},
{"ul_angle", 72u},
{"umbrella", 73u},
{"ur_angle", 74u},
{"watch", 75u},
{"xterm", 76u},
});
for (const auto& cursor_name : cursor_names) {
auto it = kMap.find(cursor_name);
if (it != kMap.end()) {
return it->second;
}
}
// Use a left pointer as a fallback.
return 0;
}
bool XCursorLoader::SupportsCreateCursor() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return connection_->render_version() >= std::pair<uint32_t, uint32_t>{0, 5};
}
bool XCursorLoader::SupportsCreateAnimCursor() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return connection_->render_version() >= std::pair<uint32_t, uint32_t>{0, 8};
}
void XCursorLoader::OnPropertyChanged(x11::Atom property,
const x11::GetPropertyResponse& value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(property, x11::Atom::RESOURCE_MANAGER);
rm_xcursor_theme_ = "";
rm_xcursor_size_ = 0;
rm_xft_dpi_ = 0;
size_t size = 0;
if (const char* resource_manager =
x11::PropertyCache::GetAs<char>(value, &size)) {
ParseXResources(std::string_view(resource_manager, size));
}
if (on_cursor_config_changed_) {
on_cursor_config_changed_.Run();
}
}
// This is ported from libxcb-cursor's parse_cursor_file.c:
// https://gitlab.freedesktop.org/xorg/lib/libxcb-cursor/-/blob/master/cursor/parse_cursor_file.c
std::vector<XCursorLoader::Image> ParseCursorFile(
scoped_refptr<base::RefCountedMemory> file,
uint32_t preferred_size) {
constexpr uint32_t kMagic = 0x72756358u;
constexpr uint32_t kImageType = 0xfffd0002u;
size_t offset = 0u;
// Reads bytes from `file` and writes them into the `dest` buffer.
auto ReadBytes = [&](base::span<uint8_t> dest) {
CHECK_EQ(dest.size() % 4u, 0u);
auto src = base::span<const uint8_t>(*file);
if (auto end = base::CheckAdd(offset, dest.size());
!end.IsValid() || end.ValueOrDie() > src.size()) {
return false;
}
dest.copy_from(src.subspan(offset, dest.size()));
offset += dest.size();
return true;
};
// Reads a single 32-bit value from `file` and writes it to `dest`.
auto ReadU32 = [&](uint32_t& dest) {
auto src = base::span(*file);
if (auto end = base::CheckAdd(offset, sizeof(dest));
!end.IsValid() || end.ValueOrDie() > src.size()) {
return false;
}
dest = base::U32FromLittleEndian(src.subspan(offset).first<sizeof(dest)>());
offset += sizeof(dest);
return true;
};
struct FileHeader {
uint32_t magic;
uint32_t header;
uint32_t version;
uint32_t ntoc;
} header;
if (!ReadU32(header.magic) || //
!ReadU32(header.header) || //
!ReadU32(header.version) || //
!ReadU32(header.ntoc) || //
header.magic != kMagic) {
return {};
}
struct TableOfContentsEntry {
uint32_t type;
uint32_t subtype;
uint32_t position;
};
std::vector<TableOfContentsEntry> toc;
for (uint32_t i = 0u; i < header.ntoc; i++) {
TableOfContentsEntry entry;
if (!ReadU32(entry.type) || //
!ReadU32(entry.subtype) || //
!ReadU32(entry.position)) {
return {};
}
toc.push_back(entry);
}
uint32_t best_size = std::numeric_limits<uint32_t>::max();
for (const auto& entry : toc) {
auto delta = [](uint32_t x, uint32_t y) {
return std::max(x, y) - std::min(x, y);
};
if (entry.type != kImageType)
continue;
if (delta(entry.subtype, preferred_size) < delta(best_size, preferred_size))
best_size = entry.subtype;
}
std::vector<XCursorLoader::Image> images;
for (const auto& entry : toc) {
if (entry.type != kImageType || entry.subtype != best_size)
continue;
offset = entry.position;
struct ChunkHeader {
uint32_t header;
uint32_t type;
uint32_t subtype;
uint32_t version;
} chunk_header;
if (!ReadU32(chunk_header.header) || //
!ReadU32(chunk_header.type) || //
!ReadU32(chunk_header.subtype) || //
!ReadU32(chunk_header.version) || //
chunk_header.type != entry.type ||
chunk_header.subtype != entry.subtype) {
return {};
}
struct ImageHeader {
uint32_t width;
uint32_t height;
uint32_t xhot;
uint32_t yhot;
uint32_t delay;
} image;
if (!ReadU32(image.width) || //
!ReadU32(image.height) || //
!ReadU32(image.xhot) || //
!ReadU32(image.yhot) || //
!ReadU32(image.delay)) {
return {};
}
// Ignore unreasonably-sized cursors to prevent allocating too much
// memory in the bitmap below.
if (image.width > 8192u || image.height > 8192u) {
return {};
}
SkBitmap bitmap;
bitmap.allocN32Pixels(image.width, image.height);
base::span<uint8_t> pixels =
// SAFETY: SkBitmap promises that getPixels() returns a pointer to
// at least as many bytes as computeByteSize().
//
// TODO(crbug.com/40284755): SkBitmap should provide a span-based
// API.
UNSAFE_TODO(base::span(static_cast<uint8_t*>(bitmap.getPixels()),
bitmap.computeByteSize()));
if (!ReadBytes(pixels)) {
return {};
}
images.push_back(XCursorLoader::Image{bitmap,
gfx::Point(image.xhot, image.yhot),
base::Milliseconds(image.delay)});
}
return images;
}
} // namespace ui