| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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 <limits> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/environment.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/sequence_checker.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/sys_byteorder.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/task_runner_util.h" |
| #include "ui/base/cursor/cursor_theme_manager.h" |
| #include "ui/base/x/x11_util.h" |
| #include "ui/gfx/x/connection.h" |
| #include "ui/gfx/x/xproto.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| // These cursor names are indexed by their ID in a cursor font. |
| constexpr const char* cursor_names[] = { |
| "X_cursor", |
| "arrow", |
| "based_arrow_down", |
| "based_arrow_up", |
| "boat", |
| "bogosity", |
| "bottom_left_corner", |
| "bottom_right_corner", |
| "bottom_side", |
| "bottom_tee", |
| "box_spiral", |
| "center_ptr", |
| "circle", |
| "clock", |
| "coffee_mug", |
| "cross", |
| "cross_reverse", |
| "crosshair", |
| "diamond_cross", |
| "dot", |
| "dotbox", |
| "double_arrow", |
| "draft_large", |
| "draft_small", |
| "draped_box", |
| "exchange", |
| "fleur", |
| "gobbler", |
| "gumby", |
| "hand1", |
| "hand2", |
| "heart", |
| "icon", |
| "iron_cross", |
| "left_ptr", |
| "left_side", |
| "left_tee", |
| "leftbutton", |
| "ll_angle", |
| "lr_angle", |
| "man", |
| "middlebutton", |
| "mouse", |
| "pencil", |
| "pirate", |
| "plus", |
| "question_arrow", |
| "right_ptr", |
| "right_side", |
| "right_tee", |
| "rightbutton", |
| "rtl_logo", |
| "sailboat", |
| "sb_down_arrow", |
| "sb_h_double_arrow", |
| "sb_left_arrow", |
| "sb_right_arrow", |
| "sb_up_arrow", |
| "sb_v_double_arrow", |
| "shuttle", |
| "sizing", |
| "spider", |
| "spraycan", |
| "star", |
| "target", |
| "tcross", |
| "top_left_arrow", |
| "top_left_corner", |
| "top_right_corner", |
| "top_side", |
| "top_tee", |
| "trek", |
| "ul_angle", |
| "umbrella", |
| "ur_angle", |
| "watch", |
| "xterm", |
| }; |
| |
| std::string GetEnv(const std::string& var) { |
| auto env = base::Environment::Create(); |
| std::string value; |
| env->GetVar(var, &value); |
| return value; |
| } |
| |
| std::string CursorPath() { |
| constexpr const char kDefaultPath[] = |
| "~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps:" |
| "/usr/X11R6/lib/X11/icons"; |
| std::string path = GetEnv("XCURSOR_PATH"); |
| return path.empty() ? kDefaultPath : 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(&components); |
| 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; |
| } |
| |
| // 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& name) { |
| constexpr const char kCursorDir[] = "cursors"; |
| constexpr const char kThemeInfo[] = "index.theme"; |
| 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(name), &contents)) |
| return base::RefCountedString::TakeString(&contents); |
| |
| if (base_themes.empty()) |
| base_themes = GetBaseThemes(theme_dir.Append(kThemeInfo)); |
| } |
| |
| for (const auto& path : base_themes) { |
| if (auto contents = ReadCursorFromTheme(path, name)) |
| return contents; |
| } |
| |
| return nullptr; |
| } |
| |
| scoped_refptr<base::RefCountedMemory> ReadCursorFile( |
| const std::string& name, |
| const std::string& rm_xcursor_theme) { |
| constexpr const char kDefaultTheme[] = "default"; |
| std::string themes[] = { |
| // The toolkit theme has the highest priority. |
| CursorThemeManager::GetInstance() |
| ? CursorThemeManager::GetInstance()->GetCursorThemeName() |
| : std::string(), |
| |
| // 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, name)) |
| return file; |
| } |
| return nullptr; |
| } |
| |
| std::vector<XCursorLoader::Image> ReadCursorImages( |
| const std::vector<std::string>& names, |
| const std::string& rm_xcursor_theme, |
| uint32_t preferred_size) { |
| // Fallback on a left pointer if possible. |
| auto names_copy = names; |
| names_copy.push_back("left_ptr"); |
| for (const auto& name : names_copy) { |
| if (auto contents = ReadCursorFile(name, rm_xcursor_theme)) { |
| auto images = ParseCursorFile(contents, preferred_size); |
| if (!images.empty()) |
| return images; |
| } |
| } |
| return {}; |
| } |
| |
| } // namespace |
| |
| XCursorLoader::XCursorLoader(x11::Connection* connection) |
| : connection_(connection) { |
| auto ver_cookie = connection_->render().QueryVersion( |
| {x11::Render::major_version, x11::Render::minor_version}); |
| auto pf_cookie = connection_->render().QueryPictFormats({}); |
| cursor_font_ = connection_->GenerateId<x11::Font>(); |
| connection_->OpenFont({cursor_font_, "cursor"}); |
| |
| std::string resource_manager; |
| if (ui::GetStringProperty(connection_->default_root(), "RESOURCE_MANAGER", |
| &resource_manager)) { |
| ParseXResources(resource_manager); |
| } |
| |
| if (auto reply = ver_cookie.Sync()) { |
| render_version_ = |
| base::Version({reply->major_version, reply->minor_version}); |
| } |
| |
| if (auto pf_reply = pf_cookie.Sync()) |
| pict_format_ = GetRenderARGBFormat(*pf_reply.reply); |
| |
| for (uint16_t i = 0; i < base::size(cursor_names); i++) |
| cursor_name_to_char_[cursor_names[i]] = i; |
| } |
| |
| XCursorLoader::~XCursorLoader() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| scoped_refptr<X11Cursor> XCursorLoader::LoadCursor( |
| const std::vector<std::string>& names) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| auto cursor = base::MakeRefCounted<X11Cursor>(); |
| if (SupportsCreateCursor()) { |
| base::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::ThreadPool(), base::MayBlock(), |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(ReadCursorImages, names, rm_xcursor_theme_, |
| GetPreferredCursorSize()), |
| base::BindOnce(&XCursorLoader::LoadCursorImpl, |
| weak_factory_.GetWeakPtr(), cursor, names)); |
| } else { |
| LoadCursorImpl(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_, image.frame_delay_ms}); |
| } |
| |
| 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>(); |
| int width = bitmap.width(); |
| int 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); |
| memcpy(vec.data(), bitmap.getPixels(), size); |
| auto* connection = x11::Connection::Get(); |
| x11::PutImageRequest put_image_request{ |
| .format = x11::ImageFormat::ZPixmap, |
| .drawable = static_cast<x11::Pixmap>(pixmap), |
| .gc = static_cast<x11::GraphicsContext>(gc), |
| .width = width, |
| .height = height, |
| .depth = 32, |
| .data = base::RefCountedBytes::TakeVector(&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, hotspot.x(), 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>& 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(names); |
| constexpr uint16_t kFontCursorFgColor = 0; |
| constexpr uint16_t kFontCursorBgColor = 65535; |
| connection_->CreateGlyphCursor( |
| {xcursor, cursor_font_, cursor_font_, 2 * core_char, 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; |
| |
| // Let the toolkit have the next say. |
| auto* manager = CursorThemeManager::GetInstance(); |
| size = manager ? manager->GetCursorThemeSize() : 0; |
| if (size > 0) |
| return size; |
| |
| // 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(const std::string& 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>& names) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| for (const auto& name : names) { |
| auto it = cursor_name_to_char_.find(name); |
| if (it != cursor_name_to_char_.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 render_version_.IsValid() && render_version_ >= base::Version("0.5"); |
| } |
| |
| bool XCursorLoader::SupportsCreateAnimCursor() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return render_version_.IsValid() && render_version_ >= base::Version("0.8"); |
| } |
| |
| // 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 = 0x72756358; |
| constexpr uint32_t kImageType = 0xfffd0002; |
| |
| const uint8_t* mem = file->data(); |
| size_t offset = 0; |
| |
| auto ReadU32s = [&](void* dest, size_t len) { |
| DCHECK_EQ(len % 4, 0u); |
| if (offset + len > file->size()) |
| return false; |
| const auto* src32 = reinterpret_cast<const uint32_t*>(mem + offset); |
| auto* dest32 = reinterpret_cast<uint32_t*>(dest); |
| for (size_t i = 0; i < len / 4; i++) |
| dest32[i] = base::ByteSwapToLE32(src32[i]); |
| offset += len; |
| return true; |
| }; |
| |
| struct FileHeader { |
| uint32_t magic; |
| uint32_t header; |
| uint32_t version; |
| uint32_t ntoc; |
| } header; |
| if (!ReadU32s(&header, sizeof(FileHeader)) || header.magic != kMagic) |
| return {}; |
| |
| struct TableOfContentsEntry { |
| uint32_t type; |
| uint32_t subtype; |
| uint32_t position; |
| }; |
| std::vector<TableOfContentsEntry> toc; |
| toc.reserve(header.ntoc); |
| for (uint32_t i = 0; i < header.ntoc; i++) { |
| TableOfContentsEntry entry; |
| if (!ReadU32s(&entry, sizeof(TableOfContentsEntry))) |
| 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 (!ReadU32s(&chunk_header, sizeof(ChunkHeader)) || |
| chunk_header.type != entry.type || |
| chunk_header.subtype != entry.subtype) { |
| continue; |
| } |
| |
| struct ImageHeader { |
| uint32_t width; |
| uint32_t height; |
| uint32_t xhot; |
| uint32_t yhot; |
| uint32_t delay; |
| } image; |
| if (!ReadU32s(&image, sizeof(ImageHeader))) |
| continue; |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(image.width, image.height); |
| if (!ReadU32s(bitmap.getPixels(), bitmap.computeByteSize())) |
| continue; |
| images.push_back(XCursorLoader::Image{ |
| bitmap, gfx::Point(image.xhot, image.yhot), image.delay}); |
| } |
| return images; |
| } |
| |
| } // namespace ui |