Files
fcitx5-m3ui/src/theme.cpp
T

1312 lines
46 KiB
C++

/*
* SPDX-FileCopyrightText: 2016-2017 CSSlayer <wengxt@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
*/
#include "theme.h"
#include <fcntl.h>
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <cairo.h>
#include <format>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gio/gio.h>
#include <gio/gunixinputstream.h>
#include <glib.h>
#include <glibconfig.h>
#include <librsvg/rsvg.h>
#include <pango/pango-font.h>
#include <pango/pango-fontmap.h>
#include <pango/pango-layout.h>
#include <pango/pango-types.h>
#include <pango/pangocairo.h>
#include "fcitx-config/iniparser.h"
#include "fcitx-config/rawconfig.h"
#include "fcitx-utils/color.h"
#include "fcitx-utils/fs.h"
#include "fcitx-utils/log.h"
#include "fcitx-utils/macros.h"
#include "fcitx-utils/misc.h"
#include "fcitx-utils/misc_p.h"
#include "fcitx-utils/rect.h"
#include "fcitx-utils/standardpaths.h"
#include "fcitx-utils/stringutils.h"
#include "fcitx-utils/utf8.h"
#include "fcitx/icontheme.h"
#include "fcitx/misc_p.h"
#include "classicui.h"
#include "colorhelper.h"
#include "common.h"
namespace fcitx::m3ui {
namespace {
inline uint32_t charWidth(uint32_t c) {
if (g_unichar_iszerowidth(c)) {
return 0;
}
return g_unichar_iswide(c) ? 2 : 1;
}
// This is heuristic, but we guarantee that we don't do crazy things with label.
std::pair<std::string, size_t> extractTextForLabel(const std::string &label) {
std::string extracted;
// We have non white space here because xkb shortDescription have things
// like fr-tg, mon-a1.
auto texts = stringutils::split(label, FCITX_WHITESPACE "-_/|");
if (texts.empty()) {
return {"", 0};
}
size_t currentWidth = 0;
for (uint32_t chr : utf8::MakeUTF8CharRange(texts[0])) {
const auto width = charWidth(chr);
if (currentWidth + width <= 3) {
extracted.append(utf8::UCS4ToUTF8(chr));
currentWidth += width;
} else {
break;
}
}
return {extracted, currentWidth};
}
cairo_status_t readFromFd(void *closure, unsigned char *data,
unsigned int length) {
int fd = *static_cast<int *>(closure);
while (length) {
auto sz = fs::safeRead(fd, data, length);
if (sz <= 0) {
return CAIRO_STATUS_READ_ERROR;
}
length -= sz;
data += sz;
}
return CAIRO_STATUS_SUCCESS;
}
ThemeImage::Pixmap pixBufToPixmap(GdkPixbuf *image) {
cairo_format_t format;
cairo_surface_t *surface;
if (gdk_pixbuf_get_n_channels(image) == 3) {
format = CAIRO_FORMAT_RGB24;
} else {
format = CAIRO_FORMAT_ARGB32;
}
surface = cairo_image_surface_create(format, gdk_pixbuf_get_width(image),
gdk_pixbuf_get_height(image));
gint width;
gint height;
guchar *gdk_pixels;
guchar *cairo_pixels;
int gdk_rowstride;
int cairo_stride;
int n_channels;
int j;
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy(surface);
return nullptr;
}
cairo_surface_flush(surface);
width = gdk_pixbuf_get_width(image);
height = gdk_pixbuf_get_height(image);
gdk_pixels = gdk_pixbuf_get_pixels(image);
gdk_rowstride = gdk_pixbuf_get_rowstride(image);
n_channels = gdk_pixbuf_get_n_channels(image);
cairo_stride = cairo_image_surface_get_stride(surface);
cairo_pixels = cairo_image_surface_get_data(surface);
for (j = height; j; j--) {
guchar *p = gdk_pixels;
guchar *q = cairo_pixels;
if (n_channels == 3) {
guchar *end = p + (static_cast<ptrdiff_t>(3) * width);
while (p < end) {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
q[0] = p[2];
q[1] = p[1];
q[2] = p[0];
q[3] = 0xFF;
#else
q[0] = 0xFF;
q[1] = p[0];
q[2] = p[1];
q[3] = p[2];
#endif
p += 3;
q += 4;
}
} else {
guchar *end = p + (static_cast<ptrdiff_t>(4) * width);
guint t1;
guint t2;
guint t3;
#define MULT(d, c, a, t) \
G_STMT_START { \
(t) = ((c) * (a)) + 0x80; \
(d) = (((t) >> 8) + (t)) >> 8; \
} \
G_STMT_END
while (p < end) {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
MULT(q[0], p[2], p[3], t1);
MULT(q[1], p[1], p[3], t2);
MULT(q[2], p[0], p[3], t3);
q[3] = p[3];
#else
q[0] = p[3];
MULT(q[1], p[0], p[3], t1);
MULT(q[2], p[1], p[3], t2);
MULT(q[3], p[2], p[3], t3);
#endif
p += 4;
q += 4;
}
#undef MULT
}
gdk_pixels += gdk_rowstride;
cairo_pixels += cairo_stride;
}
cairo_surface_mark_dirty(surface);
return ThemeImage::Pixmap{surface};
}
ThemeImage::Pixmap loadPixmap(UnixFD &file, const std::filesystem::path &path) {
if (file.fd() < 0) {
return nullptr;
}
if (path.extension() == ".png") {
int fd = file.fd();
auto *surface =
cairo_image_surface_create_from_png_stream(readFromFd, &fd);
if (!surface) {
return nullptr;
}
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
g_clear_pointer(&surface, cairo_surface_destroy);
return nullptr;
}
return ThemeImage::Pixmap{surface};
}
GObjectUniquePtr<GInputStream> stream(
g_unix_input_stream_new(file.fd(), false));
if (!stream) {
return nullptr;
}
GObjectUniquePtr<GdkPixbuf> image(
gdk_pixbuf_new_from_stream(stream.get(), nullptr, nullptr));
g_input_stream_close(stream.get(), nullptr, nullptr);
if (!image) {
return nullptr;
}
return pixBufToPixmap(image.get());
}
std::optional<std::pair<int, int>> svgSize(RsvgHandle *svg) {
double w = 0;
double h = 0;
if (rsvg_handle_get_intrinsic_size_in_pixels(svg, &w, &h)) {
int width = std::max(1, static_cast<int>(std::ceil(w)));
int height = std::max(1, static_cast<int>(std::ceil(h)));
return std::make_pair(width, height);
}
return std::nullopt;
}
ThemeImage::CairoPattern renderSvg(RsvgHandle *svg, int width, int height) {
const cairo_rectangle_t extents{0, 0, static_cast<double>(width),
static_cast<double>(height)};
ThemeImage::Pixmap surface(
cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, &extents));
if (cairo_surface_status(surface.get()) != CAIRO_STATUS_SUCCESS) {
return nullptr;
}
UniqueCPtr<cairo_t, cairo_destroy> context(cairo_create(surface.get()));
if (cairo_status(context.get()) != CAIRO_STATUS_SUCCESS) {
return nullptr;
}
const RsvgRectangle viewport{0, 0, static_cast<double>(width),
static_cast<double>(height)};
if (!rsvg_handle_render_document(svg, context.get(), &viewport, nullptr) ||
cairo_status(context.get()) != CAIRO_STATUS_SUCCESS) {
return nullptr;
}
ThemeImage::CairoPattern pattern(
cairo_pattern_create_for_surface(surface.get()));
if (cairo_pattern_status(pattern.get()) != CAIRO_STATUS_SUCCESS) {
return nullptr;
}
return pattern;
}
std::optional<ThemeImage::Svg> loadSvg(UnixFD &file) {
if (!file.isValid()) {
return std::nullopt;
}
GObjectUniquePtr<GInputStream> stream(
g_unix_input_stream_new(file.fd(), false));
if (!stream) {
return std::nullopt;
}
GObjectUniquePtr<RsvgHandle> handle(rsvg_handle_new_from_stream_sync(
stream.get(), nullptr, RSVG_HANDLE_FLAGS_NONE, nullptr, nullptr));
g_input_stream_close(stream.get(), nullptr, nullptr);
if (!handle) {
return std::nullopt;
}
auto size = svgSize(handle.get());
if (!size) {
return std::nullopt;
}
ThemeImage::Svg svg;
svg.width = size->first;
svg.height = size->second;
svg.pattern = renderSvg(handle.get(), svg.width, svg.height);
if (!svg.pattern) {
return std::nullopt;
}
return svg;
}
bool isSvgPath(const std::filesystem::path &path) {
return path.extension() == ".svg" || path.extension() == ".svgz";
}
const std::vector<std::string> &gdkPixbufSupportedFormats() {
const static std::vector<std::string> formats = []() {
std::unordered_set<std::string> exts;
std::vector<std::string> result;
// PNG is supported by cairo.
UniqueCPtr<GSList, g_slist_free> list(gdk_pixbuf_get_formats());
for (GSList *item = list.get(); item; item = g_slist_next(item)) {
gchar **extension = gdk_pixbuf_format_get_extensions(
static_cast<GdkPixbufFormat *>(item->data));
for (auto *iter = extension; iter && *iter; iter++) {
exts.insert(std::string(*iter));
}
g_strfreev(extension);
}
// Only put the common types and we make a preferred order.
for (std::string ext : {"svg", "svgz", "png", "bmp", "xpm"}) {
// png is supported by cairo.
if (ext == "png" || exts.contains(ext)) {
result.push_back("." + ext);
}
}
CLASSICUI_DEBUG() << "Supported image extensions: " << result;
return result;
}();
return formats;
}
constexpr double RoundEpsilon = 1e-3;
double pixelCeil(double f) { return std::ceil(f - RoundEpsilon); }
double pixelFloor(double f) { return std::floor(f + RoundEpsilon); }
template <typename CreatePattern>
std::optional<ThemeImage::NineTiles>
createImage(int width, int height, int marginLeft, int marginTop,
int marginRight, int marginBottom, bool retainSourceOffset,
CreatePattern createPattern) {
ThemeImage::NineTiles result;
result.width = width;
result.height = height;
const int sourceX[] = {0, marginLeft, width - marginRight, width};
const int sourceY[] = {0, marginTop, height - marginBottom, height};
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int tileWidth = sourceX[x + 1] - sourceX[x];
int tileHeight = sourceY[y + 1] - sourceY[y];
if (tileWidth <= 0 || tileHeight <= 0) {
continue;
}
auto &resultTile = result.tiles[(y * 3) + x];
resultTile.width = tileWidth;
resultTile.height = tileHeight;
if (retainSourceOffset) {
resultTile.sourceX = sourceX[x];
resultTile.sourceY = sourceY[y];
}
resultTile.pattern =
createPattern(sourceX[x], sourceY[y], tileWidth, tileHeight);
if (cairo_pattern_status(resultTile.pattern.get()) !=
CAIRO_STATUS_SUCCESS) {
return std::nullopt;
}
}
}
return result;
}
std::optional<ThemeImage::NineTiles>
createImage(cairo_pattern_t *source, int width, int height, int marginLeft = 0,
int marginTop = 0, int marginRight = 0, int marginBottom = 0) {
if (cairo_pattern_status(source) != CAIRO_STATUS_SUCCESS) {
return std::nullopt;
}
return createImage(width, height, marginLeft, marginTop, marginRight,
marginBottom, true, [source](int, int, int, int) {
return ThemeImage::CairoPattern(
cairo_pattern_reference(source));
});
}
std::optional<ThemeImage::NineTiles>
createImage(ThemeImage::Pixmap pixmap, int marginLeft = 0, int marginTop = 0,
int marginRight = 0, int marginBottom = 0, int scale = 1) {
if (!pixmap) {
return std::nullopt;
}
int sourceWidth = cairo_image_surface_get_width(pixmap.get());
int sourceHeight = cairo_image_surface_get_height(pixmap.get());
if (sourceWidth % scale || sourceHeight % scale) {
return std::nullopt;
}
int width = sourceWidth / scale;
int height = sourceHeight / scale;
return createImage(
width, height, marginLeft, marginTop, marginRight, marginBottom, false,
[surface = pixmap.get(), scale](int x, int y, int tileWidth,
int tileHeight) {
ThemeImage::Pixmap tile(cairo_surface_create_for_rectangle(
surface, x * scale, y * scale, tileWidth * scale,
tileHeight * scale));
ThemeImage::CairoPattern pattern(
cairo_pattern_create_for_surface(tile.get()));
cairo_matrix_t matrix;
cairo_matrix_init_scale(&matrix, scale, scale);
cairo_pattern_set_matrix(pattern.get(), &matrix);
cairo_pattern_set_extend(pattern.get(), CAIRO_EXTEND_PAD);
return pattern;
});
}
std::filesystem::path scaledImagePath(const std::filesystem::path &path,
int scale) {
return path.parent_path() / std::format("{}@{}x{}", path.stem().string(),
scale, path.extension().string());
}
template <typename Image, typename Load>
void loadScaledImages(std::map<int, Image> &images,
const std::filesystem::path &path, int supportedScale,
int width, int height, Load load) {
for (int scale = 2; scale <= supportedScale; scale++) {
auto scaledPath = scaledImagePath(path, scale);
auto file = StandardPaths::openPath(scaledPath);
if (auto image = load(scale, file, scaledPath)) {
if (image->width == width && image->height == height) {
images.emplace(scale, std::move(*image));
}
}
}
}
template <typename Image>
const Image *findScaledImage(const std::map<int, Image> &images, int scale) {
if (scale == 1) {
return nullptr;
}
auto iter = images.lower_bound(scale);
if (iter != images.end()) {
return &iter->second;
}
return images.empty() ? nullptr : &images.rbegin()->second;
}
void paintTile(cairo_t *c, double x, double y, double width, double height,
double alpha, const ThemeImage &image, int marginLeft,
int marginTop, int marginRight, int marginBottom) {
int resizeHeight = image.height() - marginTop - marginBottom;
int resizeWidth = image.width() - marginLeft - marginRight;
if (resizeHeight <= 0) {
resizeHeight = 1;
}
if (resizeWidth <= 0) {
resizeWidth = 1;
}
if (height < 0) {
height = resizeHeight;
}
if (width < 0) {
width = resizeWidth;
}
const auto targetResizeWidth = width - marginLeft - marginRight;
const auto targetResizeHeight = height - marginTop - marginBottom;
double gridX[] = {0.0, static_cast<double>(marginLeft),
static_cast<double>(width - marginRight), width};
double gridY[] = {0.0, static_cast<double>(marginTop),
static_cast<double>(height - marginBottom), height};
for (double &gx : gridX) {
gx += x;
}
for (double &gy : gridY) {
gy += y;
}
auto *surface = cairo_get_target(c);
double xScale;
double yScale;
cairo_surface_get_device_scale(surface, &xScale, &yScale);
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
gridX[i] = pixelFloor(gridX[i] * xScale) / xScale;
gridY[i] = pixelFloor(gridY[i] * yScale) / yScale;
} else {
gridX[i] = pixelCeil(gridX[i] * xScale) / xScale;
gridY[i] = pixelCeil(gridY[i] * yScale) / yScale;
}
}
auto part = [&](int ix, int iy) {
double dx = gridX[ix];
double dy = gridY[iy];
double dw = gridX[ix + 1] - gridX[ix];
double dh = gridY[iy + 1] - gridY[iy];
if (dw > 0 && dh > 0) {
image.paintRegion(
c, static_cast<ThemeImage::TilePosition>((iy * 3) + ix), dx, dy,
dw, dh, alpha);
}
};
/*
* 7 8 9
* 4 5 6
* 1 2 3
*/
if (marginLeft && marginBottom) {
/* part 1 */
part(0, 2);
}
if (marginRight && marginBottom) {
/* part 3 */
part(2, 2);
}
if (marginLeft && marginTop) {
/* part 7 */
part(0, 0);
}
if (marginRight && marginTop) {
/* part 9 */
part(2, 0);
}
/* part 2 & 8 */
if (marginTop && targetResizeWidth > 0) {
part(1, 0);
}
if (marginBottom && targetResizeWidth > 0) {
part(1, 2);
}
/* part 4 & 6 */
if (marginLeft && targetResizeHeight > 0) {
part(0, 1);
}
if (marginRight && targetResizeHeight > 0) {
part(2, 1);
}
/* part 5 */
if (targetResizeHeight > 0 && targetResizeWidth > 0) {
part(1, 1);
}
}
} // namespace
ThemeImage::ThemeImage(const IconTheme &iconTheme, const std::string &icon,
const std::string &label, uint32_t size,
const M3UI *classicui)
: size_(size) {
bool preferTextIcon =
!label.empty() &&
((icon == "input-keyboard" &&
hasTwoKeyboardInCurrentGroup(classicui->instance())) ||
*classicui->config().preferTextIcon);
if (!preferTextIcon && !icon.empty()) {
std::filesystem::path iconPath =
iconTheme.findIconPath(icon, size, 1, gdkPixbufSupportedFormats());
auto fd = StandardPaths::openPath(iconPath);
if (isSvgPath(iconPath)) {
if (auto svg = loadSvg(fd)) {
if (auto image = createImage(svg->pattern.get(), svg->width,
svg->height)) {
image_ = std::move(*image);
}
}
} else {
Pixmap pixmap = loadPixmap(fd, iconPath);
if (auto image = createImage(std::move(pixmap))) {
image_ = std::move(*image);
}
}
}
if (!valid()) {
Pixmap textImage(
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, size, size));
drawTextIcon(textImage.get(), label, size, classicui->config());
if (auto image = createImage(std::move(textImage))) {
image_ = std::move(*image);
}
}
}
ThemeImage::ThemeImage(const Theme &theme, const BackgroundImageConfig &cfg,
const Color &color, const Color &borderColor) {
if (!cfg.image->empty()) {
std::filesystem::path imagePath;
auto imageFile = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / theme.name() / *cfg.image,
theme.isSystemTheme() ? StandardPathsMode::System
: StandardPathsMode::Default,
&imagePath);
if (isSvgPath(imagePath)) {
if (auto svg = loadSvg(imageFile)) {
if (auto image = createImage(
svg->pattern.get(), svg->width, svg->height,
*cfg.margin->marginLeft, *cfg.margin->marginTop,
*cfg.margin->marginRight, *cfg.margin->marginBottom)) {
image_ = std::move(*image);
}
}
} else {
Pixmap pixmap = loadPixmap(imageFile, imagePath);
if (auto image = createImage(
std::move(pixmap), *cfg.margin->marginLeft,
*cfg.margin->marginTop, *cfg.margin->marginRight,
*cfg.margin->marginBottom)) {
image_ = std::move(*image);
loadScaledImages(
scaledImages_, imagePath, *theme.supportedScale,
image_->width, image_->height,
[&cfg](int scale, UnixFD &file,
const std::filesystem::path &path) {
return createImage(
loadPixmap(file, path), *cfg.margin->marginLeft,
*cfg.margin->marginTop, *cfg.margin->marginRight,
*cfg.margin->marginBottom, scale);
});
}
}
}
if (!cfg.overlay->empty()) {
std::filesystem::path imagePath;
auto imageFile = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / theme.name() / *cfg.overlay,
theme.isSystemTheme() ? StandardPathsMode::System
: StandardPathsMode::Default,
&imagePath);
if (isSvgPath(imagePath)) {
if (auto svg = loadSvg(imageFile)) {
if (auto overlay = createImage(svg->pattern.get(), svg->width,
svg->height)) {
overlay_ = std::move(overlay->tiles[4]);
}
}
} else if (auto overlay =
createImage(loadPixmap(imageFile, imagePath))) {
overlay_ = std::move(overlay->tiles[4]);
loadScaledImages(
scaledOverlays_, imagePath, *theme.supportedScale,
overlay_->width, overlay_->height,
[](int scale, UnixFD &file, const std::filesystem::path &path) {
if (auto image = createImage(loadPixmap(file, path), 0, 0,
0, 0, scale)) {
return std::optional<NineTiles::Tile>(
std::move(image->tiles[4]));
}
return std::optional<NineTiles::Tile>();
});
}
}
if (!valid()) {
constexpr auto minimumSize = 20;
auto width =
*cfg.margin->marginLeft + *cfg.margin->marginRight +
std::max(*cfg.margin->marginLeft + *cfg.margin->marginRight,
minimumSize);
auto height =
*cfg.margin->marginTop + *cfg.margin->marginBottom +
std::max(*cfg.margin->marginTop + *cfg.margin->marginBottom,
minimumSize);
auto borderWidth =
std::min({*cfg.borderWidth, *cfg.margin->marginLeft,
*cfg.margin->marginRight, *cfg.margin->marginTop,
*cfg.margin->marginBottom});
CLASSICUI_DEBUG() << "Paint background: height " << height << " width "
<< width << " border=" << borderColor
<< " border width=" << *cfg.borderWidth
<< " color=" << color;
ThemeImage::Pattern pattern;
pattern.width = width;
pattern.height = height;
pattern.borderWidth = borderWidth;
pattern.pattern.reset(cairo_pattern_create_mesh());
const auto setPatch = [&](double x0, double y0, double x1, double y1,
const Color &color) {
cairo_mesh_pattern_begin_patch(pattern.pattern.get());
cairo_mesh_pattern_move_to(pattern.pattern.get(), x0, y0);
cairo_mesh_pattern_line_to(pattern.pattern.get(), x1, y0);
cairo_mesh_pattern_line_to(pattern.pattern.get(), x1, y1);
cairo_mesh_pattern_line_to(pattern.pattern.get(), x0, y1);
for (int i = 0; i < 4; i++) {
cairo_mesh_pattern_set_corner_color_rgba(
pattern.pattern.get(), i, color.redF(), color.greenF(),
color.blueF(), color.alphaF());
}
cairo_mesh_pattern_end_patch(pattern.pattern.get());
};
if (borderWidth > 0) {
const double x[] = {0.0, static_cast<double>(borderWidth),
static_cast<double>(width - borderWidth),
static_cast<double>(width)};
const double y[] = {0.0, static_cast<double>(borderWidth),
static_cast<double>(height - borderWidth),
static_cast<double>(height)};
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 3; column++) {
setPatch(x[column], y[row], x[column + 1], y[row + 1],
(row == 1 && column == 1) ? color : borderColor);
}
}
} else {
setPatch(0, 0, width, height, color);
}
if (auto image = createImage(
pattern.pattern.get(), pattern.width, pattern.height,
*cfg.margin->marginLeft, *cfg.margin->marginTop,
*cfg.margin->marginRight, *cfg.margin->marginBottom)) {
image_ = std::move(*image);
isPattern_ = true;
}
}
}
ThemeImage::ThemeImage(const Theme &theme, const ActionImageConfig &cfg) {
if (!cfg.image->empty()) {
std::filesystem::path imagePath;
auto imageFile = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / theme.name() / *cfg.image,
theme.isSystemTheme() ? StandardPathsMode::System
: StandardPathsMode::Default,
&imagePath);
if (isSvgPath(imagePath)) {
if (auto svg = loadSvg(imageFile)) {
if (auto image = createImage(svg->pattern.get(), svg->width,
svg->height)) {
image_ = std::move(*image);
}
}
} else {
if (auto image = createImage(loadPixmap(imageFile, imagePath))) {
image_ = std::move(*image);
loadScaledImages(scaledImages_, imagePath,
*theme.supportedScale, image_->width,
image_->height,
[](int scale, UnixFD &file,
const std::filesystem::path &path) {
return createImage(loadPixmap(file, path),
0, 0, 0, 0, scale);
});
}
}
}
}
void ThemeImage::drawTextIcon(cairo_surface_t *surface,
const std::string &rawLabel, uint32_t size,
const ClassicUIConfig &config) {
auto [label, textWidth] = extractTextForLabel(rawLabel);
auto *cr = cairo_create(surface);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairoSetSourceColor(cr, Color("#00000000"));
cairo_paint(cr);
int pixelSize = size * 0.7 * (textWidth >= 3 ? (2.0 / textWidth) : 1.0);
if (pixelSize < 0) {
pixelSize = 1;
}
auto *fontMap = pango_cairo_font_map_get_default();
GObjectUniquePtr<PangoContext> context(
pango_font_map_create_context(fontMap));
GObjectUniquePtr<PangoLayout> layout(pango_layout_new(context.get()));
pango_layout_set_single_paragraph_mode(layout.get(), true);
pango_layout_set_text(layout.get(), label.c_str(), label.size());
PangoRectangle rect;
PangoFontDescription *desc =
pango_font_description_from_string(config.trayFont->c_str());
pango_font_description_set_absolute_size(desc, pixelSize * PANGO_SCALE);
pango_layout_set_font_description(layout.get(), desc);
pango_font_description_free(desc);
pango_layout_get_pixel_extents(layout.get(), &rect, nullptr);
cairo_translate(cr, ((size - rect.width) * 0.5) - rect.x,
((size - rect.height) * 0.5) - rect.y);
if (config.trayBorderColor->alpha()) {
cairo_save(cr);
cairoSetSourceColor(cr, *config.trayBorderColor);
pango_cairo_layout_path(cr, layout.get());
cairo_set_line_width(cr, std::min(4, (pixelSize + 4) / 8));
cairo_stroke(cr);
cairo_restore(cr);
}
cairo_save(cr);
cairoSetSourceColor(cr, *config.trayTextColor);
pango_cairo_show_layout(cr, layout.get());
cairo_restore(cr);
cairo_destroy(cr);
}
void ThemeImage::paintRegion(cairo_t *c, TilePosition tile, double destX,
double destY, double destWidth, double destHeight,
double alpha, bool overlay) const {
if (destWidth <= 0 || destHeight <= 0) {
return;
}
if ((overlay && !overlay_) || (!overlay && !image_)) {
return;
}
const auto scale = [&]() {
double xScale;
double yScale;
cairo_surface_get_device_scale(cairo_get_target(c), &xScale, &yScale);
return static_cast<int>(std::ceil(std::max(xScale, yScale)));
}();
const NineTiles::Tile *source = nullptr;
if (overlay) {
if (auto scaledOverlay = findScaledImage(scaledOverlays_, scale)) {
source = scaledOverlay;
} else {
source = &*overlay_;
}
} else {
if (auto scaledImage = findScaledImage(scaledImages_, scale)) {
source = &scaledImage->tiles[static_cast<std::size_t>(tile)];
} else {
source = &image_->tiles[static_cast<std::size_t>(tile)];
}
}
if (!source->pattern) {
return;
}
cairo_save(c);
cairo_rectangle(c, destX, destY, destWidth, destHeight);
cairo_clip(c);
cairo_translate(c, destX, destY);
cairo_scale(c, destWidth / source->width, destHeight / source->height);
cairo_translate(c, -source->sourceX, -source->sourceY);
cairo_set_source(c, source->pattern.get());
cairo_paint_with_alpha(c, alpha);
cairo_restore(c);
}
Theme::Theme() : iconTheme_(IconTheme::defaultIconThemeName()) {}
Theme::~Theme() {}
bool Theme::isSystemThemeName(std::string_view themeName) {
return themeName == "default" || themeName == "default-dark";
}
const ThemeImage &Theme::loadBackground(const BackgroundImageConfig &cfg) {
if (auto *image = findValue(backgroundImageTable_, &cfg)) {
return *image;
}
Color color;
Color borderColor;
if (&cfg == &*inputPanel->background) {
color = inputPanelBackground_;
borderColor = inputPanelBorder_;
} else if (&cfg == &*inputPanel->highlight) {
color = inputPanelHighlightCandidateBackground_;
borderColor = inputPanelHighlightCandidateBorder_;
} else if (&cfg == &*menu->background) {
color = menuBackground_;
borderColor = menuBorder_;
} else if (&cfg == &*menu->highlight) {
color = menuSelectedItemBackground_;
borderColor = menuSelectedItemBorder_;
} else if (&cfg == &*menu->separator) {
color = menuSeparator_;
borderColor = *cfg.borderColor;
} else {
color = *cfg.color;
borderColor = *cfg.borderColor;
}
auto result = backgroundImageTable_.emplace(
std::piecewise_construct, std::forward_as_tuple(&cfg),
std::forward_as_tuple(*this, cfg, color, borderColor));
assert(result.second);
return result.first->second;
}
const ThemeImage &Theme::loadAction(const ActionImageConfig &cfg) {
if (auto *image = findValue(actionImageTable_, &cfg)) {
return *image;
}
auto result = actionImageTable_.emplace(std::piecewise_construct,
std::forward_as_tuple(&cfg),
std::forward_as_tuple(*this, cfg));
assert(result.second);
return result.first->second;
}
const ThemeImage &Theme::loadImage(const std::string &icon,
const std::string &label, uint32_t size,
const M3UI *classicui) {
auto &map = trayImageTable_;
auto name = stringutils::concat("icon:", icon, "label:", label);
if (auto *image = findValue(map, name)) {
if (image->size() == size) {
return *image;
}
map.erase(name);
}
auto result = map.emplace(
std::piecewise_construct, std::forward_as_tuple(name),
std::forward_as_tuple(iconTheme_, icon, label, size, classicui));
assert(result.second);
return result.first->second;
}
void Theme::paint(cairo_t *c, const BackgroundImageConfig &cfg, double dx,
double dy, double width, double height, double alpha) {
const ThemeImage &image = loadBackground(cfg);
auto marginTop = *cfg.margin->marginTop;
auto marginBottom = *cfg.margin->marginBottom;
auto marginLeft = *cfg.margin->marginLeft;
auto marginRight = *cfg.margin->marginRight;
paintTile(c, dx, dy, width, height, alpha, image, marginLeft, marginTop,
marginRight, marginBottom);
if (!image.hasOverlay()) {
return;
}
Rect clipRect;
auto clipWidth = width - *cfg.overlayClipMargin->marginLeft -
*cfg.overlayClipMargin->marginRight;
auto clipHeight = height - *cfg.overlayClipMargin->marginTop -
*cfg.overlayClipMargin->marginBottom;
if (clipWidth <= 0 || clipHeight <= 0) {
return;
}
clipRect
.setPosition(*cfg.overlayClipMargin->marginLeft,
*cfg.overlayClipMargin->marginTop)
.setSize(clipWidth, clipHeight);
int x = 0;
int y = 0;
switch (*cfg.gravity) {
case Gravity::TopLeft:
case Gravity::CenterLeft:
case Gravity::BottomLeft:
x = *cfg.overlayOffsetX;
break;
case Gravity::TopCenter:
case Gravity::Center:
case Gravity::BottomCenter:
x = ((width - image.overlayWidth()) / 2) + *cfg.overlayOffsetX;
break;
case Gravity::TopRight:
case Gravity::CenterRight:
case Gravity::BottomRight:
x = width - image.overlayWidth() - *cfg.overlayOffsetX;
break;
}
switch (*cfg.gravity) {
case Gravity::TopLeft:
case Gravity::TopCenter:
case Gravity::TopRight:
y = *cfg.overlayOffsetY;
break;
case Gravity::CenterLeft:
case Gravity::Center:
case Gravity::CenterRight:
y = ((height - image.overlayHeight()) / 2) + *cfg.overlayOffsetY;
break;
case Gravity::BottomLeft:
case Gravity::BottomCenter:
case Gravity::BottomRight:
y = height - image.overlayHeight() - *cfg.overlayOffsetY;
break;
}
Rect rect;
rect.setPosition(x, y).setSize(image.overlayWidth(), image.overlayHeight());
Rect finalRect = rect.intersected(clipRect);
if (finalRect.isEmpty()) {
return;
}
if (*cfg.hideOverlayIfOversize && !clipRect.contains(rect)) {
return;
}
cairo_save(c);
cairo_set_operator(c, CAIRO_OPERATOR_OVER);
cairo_rectangle(c, finalRect.left(), finalRect.top(), finalRect.width(),
finalRect.height());
cairo_clip(c);
image.paintRegion(c, ThemeImage::TilePosition::Center, x, y,
image.overlayWidth(), image.overlayHeight(), alpha, true);
cairo_restore(c);
}
void Theme::paint(cairo_t *c, const ActionImageConfig &cfg, double alpha) {
const ThemeImage &image = loadAction(cfg);
int height = image.height();
int width = image.width();
image.paintRegion(c, ThemeImage::TilePosition::Center, 0, 0, width, height,
alpha);
}
void Theme::reset() {
trayImageTable_.clear();
backgroundImageTable_.clear();
actionImageTable_.clear();
}
void Theme::load(std::string_view name) {
reset();
isSystemTheme_ = isSystemThemeName(name);
ThemeConfig config;
copyHelper(config);
// Reset the default value to state.
syncDefaultValueToCurrent();
if (auto themeConfigFile = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / name / "theme.conf",
StandardPathsMode::System);
themeConfigFile.isValid()) {
RawConfig themeConfig;
readFromIni(themeConfig, themeConfigFile.fd());
Configuration::load(themeConfig, true);
} else {
// No sys file, reset default value.
ThemeConfig config;
copyHelper(config);
}
syncDefaultValueToCurrent();
if (!isSystemTheme_) {
// For system theme, we don't load user file, as user file is only for
// custom theme.
if (auto themeConfigFile = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / name / "theme.conf",
StandardPathsMode::User);
themeConfigFile.isValid()) {
// Has user file, load user file data.
RawConfig themeConfig;
readFromIni(themeConfig, themeConfigFile.fd());
Configuration::load(themeConfig, true);
}
}
name_ = name;
maskConfig_ = *inputPanel->background;
maskConfig_.overlay.setValue("");
maskConfig_.image.setValue(*inputPanel->blurMask);
menuBlurMaskConfig_ = *menu->background;
menuBlurMaskConfig_.overlay.setValue("");
menuBlurMaskConfig_.image.setValue(*menu->blurMask);
accentColorFields_ = std::unordered_set<ColorField>(
accentColor.value().begin(), accentColor.value().end());
}
void Theme::load(std::string_view name, const RawConfig &rawConfig) {
reset();
isSystemTheme_ = isSystemThemeName(name);
Configuration::load(rawConfig, true);
name_ = name;
}
bool Theme::setIconTheme(const std::string &name) {
if (iconTheme_.internalName() != name) {
CLASSICUI_DEBUG() << "New Icon theme: " << name;
iconTheme_ = IconTheme(name);
trayImageTable_.clear();
return true;
}
return false;
}
std::vector<Rect> Theme::mask(const BackgroundImageConfig &cfg, int width,
int height) {
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> mask(
cairo_image_surface_create(CAIRO_FORMAT_A1, width, height));
auto *c = cairo_create(mask.get());
cairo_set_operator(c, CAIRO_OPERATOR_SOURCE);
paint(c, cfg, 0, 0, width, height, 1);
cairo_destroy(c);
UniqueCPtr<cairo_region_t, cairo_region_destroy> region(
cairo_region_create());
cairo_rectangle_int_t rect;
#define AddSpan \
do { \
rect.x = prev1; \
rect.y = y; \
rect.width = x - prev1; \
rect.height = 1; \
cairo_region_union_rectangle(region.get(), &rect); \
} while (0)
const uint8_t zero = 0;
const auto *data = cairo_image_surface_get_data(mask.get());
auto cairo_stride = cairo_image_surface_get_stride(mask.get());
constexpr bool little = G_BYTE_ORDER == G_LITTLE_ENDIAN;
int x;
int y;
for (y = 0; y < height; ++y) {
uint8_t all = zero;
int prev1 = -1;
for (x = 0; x < width;) {
uint8_t byte = data[x / 8];
if (x > width - 8 || byte != all) {
if constexpr (little) {
for (int b = 8; b > 0 && x < width; --b) {
if (!(byte & 0x01) == !all) {
// More of the same
} else {
// A change.
if (all != zero) {
AddSpan;
all = zero;
} else {
prev1 = x;
all = ~zero;
}
}
byte >>= 1;
++x;
}
} else {
for (int b = 8; b > 0 && x < width; --b) {
if (!(byte & 0x80) == !all) {
// More of the same
} else {
// A change.
if (all != zero) {
AddSpan;
all = zero;
} else {
prev1 = x;
all = ~zero;
}
}
byte <<= 1;
++x;
}
}
} else {
x += 8;
}
}
if (all != zero) {
AddSpan;
}
data += cairo_stride;
}
#undef AddSpan
std::vector<Rect> result;
for (int i = 0, e = cairo_region_num_rectangles(region.get()); i < e; i++) {
cairo_region_get_rectangle(region.get(), i, &rect);
result.push_back(Rect()
.setPosition(rect.x, rect.y)
.setSize(rect.width, rect.height));
}
return result;
}
void Theme::applyMonet(const MonetColors &c) {
// A classic theme draws its panel from images, there is nothing to recolour.
if (inputPanel->surfaceColor->alpha() == 0) {
return;
}
auto *panel = inputPanel.mutableValue();
panel->surfaceColor.setValue(c.surface);
panel->outlineColor.setValue(c.outline);
panel->highlightFillColor.setValue(c.highlightFill);
panel->normalColor.setValue(c.normal);
panel->highlightColor.setValue(c.highlight);
panel->highlightBackgroundColor.setValue(c.highlightBackground);
panel->highlightCandidateColor.setValue(c.highlightCandidate);
panel->candidateLabelColor.setValue(c.candidateLabel);
panel->highlightCandidateLabelColor.setValue(c.highlightCandidateLabel);
panel->candidateCommentColor.setValue(c.candidateComment);
panel->highlightCandidateCommentColor.setValue(c.highlightCandidateComment);
}
void Theme::populateColor(std::optional<Color> accent) {
inputPanelBackground_ = *inputPanel->background->color;
inputPanelBorder_ = *inputPanel->background->borderColor;
inputPanelHighlightCandidateBackground_ = *inputPanel->highlight->color;
inputPanelHighlightCandidateBorder_ = *inputPanel->highlight->borderColor;
inputPanelHighlight_ = *inputPanel->highlightBackgroundColor;
inputPanelText_ = *inputPanel->normalColor;
inputPanelHighlightText_ = *inputPanel->highlightColor;
inputPanelHighlightCandidateText_ = *inputPanel->highlightCandidateColor;
auto inputPanelCandidateLabelText = *inputPanel->candidateLabelColor;
auto inputPanelHighlightCandidateLabelText =
*inputPanel->highlightCandidateLabelColor;
auto inputPanelCandidateCommentText = *inputPanel->candidateCommentColor;
auto inputPanelHighlightCandidateCommentText =
*inputPanel->highlightCandidateCommentColor;
menuBackground_ = *menu->background->color;
menuBorder_ = *menu->background->borderColor;
menuSelectedItemBackground_ = *menu->highlight->color;
menuSelectedItemBorder_ = *menu->highlight->borderColor;
menuSeparator_ = *menu->separator->color;
menuText_ = *menu->normalColor;
menuSelectedItemText_ = *menu->highlightTextColor;
if (accent) {
auto [foreground, foregroundDim] = accentForeground(*accent);
for (auto field : accentColorFields_) {
switch (field) {
case ColorField::InputPanel_Background:
inputPanelBackground_ = *accent;
inputPanelText_ = foreground;
if (inputPanelCandidateLabelText) {
inputPanelCandidateLabelText = foreground;
}
if (inputPanelCandidateCommentText) {
inputPanelCandidateCommentText = foregroundDim;
}
break;
case ColorField::InputPanel_Border:
inputPanelBorder_ = *accent;
break;
case ColorField::InputPanel_HighlightCandidateBackground:
inputPanelHighlightCandidateBackground_ = *accent;
inputPanelHighlightCandidateText_ = foreground;
if (inputPanelHighlightCandidateLabelText) {
inputPanelHighlightCandidateLabelText = foreground;
}
if (inputPanelHighlightCandidateCommentText) {
inputPanelHighlightCandidateCommentText = foregroundDim;
}
break;
case ColorField::InputPanel_HighlightCandidateBorder:
inputPanelHighlightCandidateBorder_ = *accent;
break;
case ColorField::InputPanel_Highlight:
inputPanelHighlight_ = *accent;
inputPanelHighlightText_ = foreground;
break;
case ColorField::Menu_Background:
menuBackground_ = *accent;
menuText_ = foreground;
break;
case ColorField::Menu_Border:
menuBorder_ = *accent;
break;
case ColorField::Menu_SelectedItemBackground:
menuSelectedItemBackground_ = *accent;
menuSelectedItemText_ = foreground;
break;
case ColorField::Menu_SelectedItemBorder:
menuSelectedItemBorder_ = *accent;
break;
case ColorField::Menu_Separator:
menuSeparator_ = *accent;
break;
}
}
}
if (inputPanelCandidateLabelText) {
inputPanelCandidateLabelText_ = *inputPanelCandidateLabelText;
} else {
inputPanelCandidateLabelText_ = inputPanelText_;
}
if (inputPanelHighlightCandidateLabelText) {
inputPanelHighlightCandidateLabelText_ =
*inputPanelHighlightCandidateLabelText;
} else {
inputPanelHighlightCandidateLabelText_ =
inputPanelHighlightCandidateText_;
}
if (inputPanelCandidateCommentText) {
inputPanelCandidateCommentText_ = *inputPanelCandidateCommentText;
} else {
inputPanelCandidateCommentText_ = inputPanelText_;
inputPanelCandidateCommentText_.setAlphaF(
inputPanelCandidateCommentText_.alphaF() * 0.6);
}
if (inputPanelHighlightCandidateCommentText) {
inputPanelHighlightCandidateCommentText_ =
*inputPanelHighlightCandidateCommentText;
} else {
inputPanelHighlightCandidateCommentText_ =
inputPanelHighlightCandidateText_;
inputPanelHighlightCandidateCommentText_.setAlphaF(
inputPanelHighlightCandidateCommentText_.alphaF() * 0.6);
}
}
} // namespace fcitx::m3ui