From 9ae27423466c411cdca2ce6679343cd184cd35a5 Mon Sep 17 00:00:00 2001 From: 1024th Date: Sat, 1 Aug 2026 16:05:40 +0800 Subject: [PATCH] Add HiDPI support to classic UI XCB windows --- src/ui/classic/xcbinputwindow.cpp | 62 ++++++++++++++++---------- src/ui/classic/xcbinputwindow.h | 1 + src/ui/classic/xcbmenu.cpp | 46 +++++++++---------- src/ui/classic/xcbmenu.h | 1 + src/ui/classic/xcbwindow.cpp | 74 ++++++++++++++++++++++++++----- src/ui/classic/xcbwindow.h | 11 +++++ 6 files changed, 136 insertions(+), 59 deletions(-) diff --git a/src/ui/classic/xcbinputwindow.cpp b/src/ui/classic/xcbinputwindow.cpp index 6746748b5..93ecd57f2 100644 --- a/src/ui/classic/xcbinputwindow.cpp +++ b/src/ui/classic/xcbinputwindow.cpp @@ -77,11 +77,13 @@ int XCBInputWindow::calculatePositionX(const Rect &cursorRect, int x = cursorRect.left(); auto &theme = parent_->theme(); - int leftSW = theme.inputPanel->shadowMargin->marginLeft.value(); - int rightSW = theme.inputPanel->shadowMargin->marginRight.value(); + int leftSW = + physicalFromLogical(theme.inputPanel->shadowMargin->marginLeft.value()); + int rightSW = physicalFromLogical( + theme.inputPanel->shadowMargin->marginRight.value()); - int actualWidth = width() - leftSW - rightSW; - actualWidth = actualWidth <= 0 ? width() : actualWidth; + int actualWidth = physicalWidth_ - leftSW - rightSW; + actualWidth = actualWidth <= 0 ? physicalWidth_ : actualWidth; if (closestScreen != nullptr) { int newX = std::max(x, closestScreen->left()); @@ -105,11 +107,13 @@ int XCBInputWindow::calculatePositionY(const Rect &cursorRect, int y = cursorRect.top(); auto &theme = parent_->theme(); - int topSW = theme.inputPanel->shadowMargin->marginTop.value(); - int bottomSW = theme.inputPanel->shadowMargin->marginBottom.value(); + int topSW = + physicalFromLogical(theme.inputPanel->shadowMargin->marginTop.value()); + int bottomSW = physicalFromLogical( + theme.inputPanel->shadowMargin->marginBottom.value()); - int actualHeight = height() - topSW - bottomSW; - actualHeight = actualHeight <= 0 ? height() : actualHeight; + int actualHeight = physicalHeight_ - topSW - bottomSW; + actualHeight = actualHeight <= 0 ? physicalHeight_ : actualHeight; if (closestScreen != nullptr) { int h = cursorRect.height(); @@ -117,15 +121,17 @@ int XCBInputWindow::calculatePositionY(const Rect &cursorRect, if (y < closestScreen->top()) { newY = closestScreen->top(); } else { - newY = y + (h ? h : (10 * ((dpi_ < 0 ? 96.0 : dpi_) / 96.0))); + newY = y + (h ? h : physicalFromLogical(10)); } // Try flip y. if (newY + actualHeight > closestScreen->bottom()) { if (newY > closestScreen->bottom()) { - newY = closestScreen->bottom() - actualHeight - 40; + newY = closestScreen->bottom() - actualHeight - + physicalFromLogical(40); } else { /* better position the window */ - newY = newY - actualHeight - ((h == 0) ? 40 : h); + newY = newY - actualHeight - + ((h == 0) ? physicalFromLogical(40) : h); } // If after flip, top is out of the screen, we still prefer the top @@ -165,9 +171,12 @@ void XCBInputWindow::updateDPI(InputContext *inputContext) { dpi_ = ui_->dpiByPosition(inputContext->cursorRect().left(), inputContext->cursorRect().top()); - setFontDPI(dpi_); + // Let Cairo device scale handle HiDPI so Pango keeps logical font sizes. + setFontDPI(-1); } +double XCBInputWindow::scale() const { return scaleForDPI(dpi_); } + void XCBInputWindow::update(InputContext *inputContext) { if (!wid_) { return; @@ -185,15 +194,19 @@ void XCBInputWindow::update(InputContext *inputContext) { return; } - if (width != this->width() || height != this->height()) { - resize(width, height); + auto oldPhysicalWidth = physicalWidth_; + auto oldPhysicalHeight = physicalHeight_; + resize(width, height); + if (physicalWidth_ != oldPhysicalWidth || + physicalHeight_ != oldPhysicalHeight) { if (atomBlur_) { - Rect rect(0, 0, width, height); - shrink(rect, *ui_->parent()->theme().inputPanel->blurMargin); + Rect logicalRect(0, 0, width, height); + shrink(logicalRect, *ui_->parent()->theme().inputPanel->blurMargin); if (!*ui_->parent()->theme().inputPanel->enableBlur || - rect.isEmpty()) { + logicalRect.isEmpty()) { xcb_delete_property(ui_->connection(), wid_, atomBlur_); } else { + auto rect = physicalFromLogical(logicalRect); std::vector data; if (ui_->parent()->theme().inputPanel->blurMask->empty()) { data.push_back(rect.left()); @@ -208,10 +221,11 @@ void XCBInputWindow::update(InputContext *inputContext) { auto region = parent_->theme().mask( parent_->theme().maskConfig(), width, height); for (const auto &rect : region) { - data.push_back(rect.left()); - data.push_back(rect.top()); - data.push_back(rect.width()); - data.push_back(rect.height()); + auto physicalRect = physicalFromLogical(rect); + data.push_back(physicalRect.left()); + data.push_back(physicalRect.top()); + data.push_back(physicalRect.width()); + data.push_back(physicalRect.height()); } xcb_change_property(ui_->connection(), XCB_PROP_MODE_REPLACE, wid_, atomBlur_, @@ -250,7 +264,8 @@ bool XCBInputWindow::filterEvent(xcb_generic_event_t *event) { break; } if (buttonPress->detail == XCB_BUTTON_INDEX_1) { - click(buttonPress->event_x, buttonPress->event_y); + click(logicalFromPhysical(buttonPress->event_x), + logicalFromPhysical(buttonPress->event_y)); } else if (buttonPress->detail == XCB_BUTTON_INDEX_4) { wheel(/*up=*/true); } else if (buttonPress->detail == XCB_BUTTON_INDEX_5) { @@ -261,7 +276,8 @@ bool XCBInputWindow::filterEvent(xcb_generic_event_t *event) { case XCB_MOTION_NOTIFY: { auto *motion = reinterpret_cast(event); if (motion->event == wid_) { - if (hover(motion->event_x, motion->event_y)) { + if (hover(logicalFromPhysical(motion->event_x), + logicalFromPhysical(motion->event_y))) { repaint(); } return true; diff --git a/src/ui/classic/xcbinputwindow.h b/src/ui/classic/xcbinputwindow.h index 4f89d3f85..0a6af383b 100644 --- a/src/ui/classic/xcbinputwindow.h +++ b/src/ui/classic/xcbinputwindow.h @@ -31,6 +31,7 @@ class XCBInputWindow : public XCBWindow, protected InputWindow { private: void repaint(); + double scale() const override; const Rect *getClosestScreen(const Rect &cursorRect) const; int calculatePositionX(const Rect &cursorRect, const Rect *closestScreen) const; diff --git a/src/ui/classic/xcbmenu.cpp b/src/ui/classic/xcbmenu.cpp index c267872e1..7ff1bf783 100644 --- a/src/ui/classic/xcbmenu.cpp +++ b/src/ui/classic/xcbmenu.cpp @@ -105,8 +105,9 @@ bool XCBMenu::filterEvent(xcb_generic_event_t *event) { } if (auto *menu = childByPosition(buttonPress->root_x, buttonPress->root_y)) { - menu->handleButtonPress(buttonPress->root_x - menu->x_, - buttonPress->root_y - menu->y_); + menu->handleButtonPress( + menu->logicalFromPhysical(buttonPress->root_x - menu->x_), + menu->logicalFromPhysical(buttonPress->root_y - menu->y_)); } else { hideAll(); return true; @@ -120,8 +121,9 @@ bool XCBMenu::filterEvent(xcb_generic_event_t *event) { } if (auto *menu = childByPosition(motion->root_x, motion->root_y)) { - menu->handleMotionNotify(motion->root_x - menu->x_, - motion->root_y - menu->y_); + menu->handleMotionNotify( + menu->logicalFromPhysical(motion->root_x - menu->x_), + menu->logicalFromPhysical(motion->root_y - menu->y_)); } return true; } @@ -279,7 +281,7 @@ XCBMenu *XCBMenu::childByPosition(int rootX, int rootY) { while (result) { Rect rect; rect.setPosition(result->x_, result->y_) - .setSize(result->width_, result->height_); + .setSize(result->physicalWidth_, result->physicalHeight_); if (rect.contains(rootX, rootY)) { break; } @@ -353,7 +355,8 @@ void XCBMenu::setHoveredIndex(int idx) { // FCITX_INFO() << this << " in timer show submenu " // << newMenu; newMenu->show( - item.first->region_.translated(x_, y_), + physicalFromLogical(item.first->region_) + .translated(x_, y_), ConstrainAdjustment::Slide); } } else { @@ -385,18 +388,14 @@ std::pair XCBMenu::actionAt(size_t index) { void XCBMenu::updateDPI(int x, int y) { dpi_ = ui_->dpiByPosition(x, y); - // Unlike pango cairo context, Cairo font map does not accept negative dpi. - // Restore to default value instead. - if (dpi_ < 0) { - pango_cairo_font_map_set_resolution( - PANGO_CAIRO_FONT_MAP(fontMap_.get()), fontMapDefaultDPI_); - } else { - pango_cairo_font_map_set_resolution( - PANGO_CAIRO_FONT_MAP(fontMap_.get()), dpi_); - } - pango_cairo_context_set_resolution(context_.get(), dpi_); + // Let Cairo device scale handle HiDPI so Pango keeps logical font sizes. + pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontMap_.get()), + fontMapDefaultDPI_); + pango_cairo_context_set_resolution(context_.get(), -1); } +double XCBMenu::scale() const { return scaleForDPI(dpi_); } + void XCBMenu::update() { auto *ic = lastRelevantIc(); if (!ic) { @@ -501,7 +500,7 @@ void XCBMenu::update() { .setSize(maxItemWidth + *highlightMargin.marginLeft + *highlightMargin.marginRight, maxItemHeight + *highlightMargin.marginTop + - *highlightMargin.marginTop); + *highlightMargin.marginBottom); item.layoutX_ = width + *textMargin.marginLeft + (hasCheckable ? checkBox.width() : 0); item.layoutY_ = height + *textMargin.marginTop + @@ -664,20 +663,19 @@ void XCBMenu::show(Rect rect, ConstrainAdjustment adjustY) { x = x + rect.width(); if (closestScreen) { - - if (x + width() > closestScreen->right()) { - x = rect.left() - width(); + if (x + physicalWidth_ > closestScreen->right()) { + x = rect.left() - physicalWidth_; } switch (adjustY) { case ConstrainAdjustment::Slide: - if (y + height() > closestScreen->bottom()) { - y = closestScreen->bottom() - height(); + if (y + physicalHeight_ > closestScreen->bottom()) { + y = closestScreen->bottom() - physicalHeight_; } break; case ConstrainAdjustment::Flip: - if (y + height() > closestScreen->bottom()) { - y = rect.top() - height(); + if (y + physicalHeight_ > closestScreen->bottom()) { + y = rect.top() - physicalHeight_; } break; }; diff --git a/src/ui/classic/xcbmenu.h b/src/ui/classic/xcbmenu.h index 297fd17de..59beb25f7 100644 --- a/src/ui/classic/xcbmenu.h +++ b/src/ui/classic/xcbmenu.h @@ -92,6 +92,7 @@ class XCBMenu : public XCBWindow, public TrackableObject { void setHoveredIndex(int idx); void setChild(XCBMenu *child); void updateDPI(int x, int y); + double scale() const override; std::pair actionAt(size_t index); MenuPool *pool_; diff --git a/src/ui/classic/xcbwindow.cpp b/src/ui/classic/xcbwindow.cpp index 235c9de42..fd1c5199a 100644 --- a/src/ui/classic/xcbwindow.cpp +++ b/src/ui/classic/xcbwindow.cpp @@ -6,6 +6,8 @@ */ #include "xcbwindow.h" +#include +#include #include #include #include @@ -77,9 +79,12 @@ void XCBWindow::createWindow(xcb_visualid_t vid, bool overrideRedirect) { params.colormap = colorMap; vid_ = vid; + physicalWidth_ = physicalSizeFromLogical(width_); + physicalHeight_ = physicalSizeFromLogical(height_); + auto cookie = xcb_aux_create_window_checked( - conn, depth, wid_, screen->root, 0, 0, width_, height_, 0, - XCB_WINDOW_CLASS_INPUT_OUTPUT, vid, valueMask, ¶ms); + conn, depth, wid_, screen->root, 0, 0, physicalWidth_, physicalHeight_, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, vid, valueMask, ¶ms); if (auto error = makeUniqueCPtr(xcb_request_check(conn, cookie))) { CLASSICUI_DEBUG() << "Create window failed: " << static_cast(error->error_code) << " " << vid @@ -105,8 +110,10 @@ void XCBWindow::createWindow(xcb_visualid_t vid, bool overrideRedirect) { conn, wid_, vid ? xcb_aux_find_visual_by_id(screen, vid) : xcb_aux_find_visual_by_id(screen, screen->root_visual), - width_, height_)); + physicalWidth_, physicalHeight_)); if (surface_) { + auto s = scale(); + cairo_surface_set_device_scale(surface_.get(), s, s); ui_->setCairoDevice(cairo_surface_get_device(surface_.get())); } contentSurface_.reset(); @@ -132,23 +139,38 @@ void XCBWindow::destroyWindow() { } void XCBWindow::resize(unsigned int width, unsigned int height) { - const uint32_t vals[2] = {width, height}; - xcb_configure_window(ui_->connection(), wid_, - XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, - vals); - cairo_xcb_surface_set_size(surface_.get(), width, height); + auto s = scale(); + auto newPhysicalWidth = physicalSizeFromLogical(width); + auto newPhysicalHeight = physicalSizeFromLogical(height); + if (newPhysicalWidth != physicalWidth_ || + newPhysicalHeight != physicalHeight_) { + const uint32_t vals[2] = {static_cast(newPhysicalWidth), + static_cast(newPhysicalHeight)}; + xcb_configure_window(ui_->connection(), wid_, + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + vals); + cairo_xcb_surface_set_size(surface_.get(), newPhysicalWidth, + newPhysicalHeight); + physicalWidth_ = newPhysicalWidth; + physicalHeight_ = newPhysicalHeight; + } + cairo_surface_set_device_scale(surface_.get(), s, s); Window::resize(width, height); - CLASSICUI_DEBUG() << "Resize: " << width << " " << height; + CLASSICUI_DEBUG() << "Resize: " << width << " " << height << " scale " << s + << " physical " << physicalWidth_ << " " + << physicalHeight_; } cairo_surface_t *XCBWindow::prerender() { #if 1 contentSurface_.reset(cairo_surface_create_similar_image( - surface_.get(), CAIRO_FORMAT_ARGB32, width(), height())); + surface_.get(), CAIRO_FORMAT_ARGB32, physicalWidth_, physicalHeight_)); #else - contentSurface_.reset( - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width(), height())); + contentSurface_.reset(cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, physicalWidth_, physicalHeight_)); #endif + auto s = scale(); + cairo_surface_set_device_scale(contentSurface_.get(), s, s); return contentSurface_.get(); } @@ -160,4 +182,32 @@ void XCBWindow::render() { cairo_destroy(cr); CLASSICUI_DEBUG() << "Render"; } + +int XCBWindow::logicalFromPhysical(int value) const { + return static_cast(std::floor(value / scale())); +} + +int XCBWindow::physicalFromLogical(int value) const { + return static_cast(std::lround(value * scale())); +} + +int XCBWindow::physicalSizeFromLogical(int size) const { + return std::max(1, physicalFromLogical(size)); +} + +Rect XCBWindow::physicalFromLogical(const Rect &rect) const { + return Rect() + .setLeft(physicalFromLogical(rect.left())) + .setTop(physicalFromLogical(rect.top())) + .setRight(physicalFromLogical(rect.right())) + .setBottom(physicalFromLogical(rect.bottom())); +} + +double XCBWindow::scaleForDPI(int dpi) { + if (dpi <= 0) { + return 1.0; + } + return static_cast(dpi) / 96.0; +} + } // namespace fcitx::classicui diff --git a/src/ui/classic/xcbwindow.h b/src/ui/classic/xcbwindow.h index 2ff145ccd..2a6353701 100644 --- a/src/ui/classic/xcbwindow.h +++ b/src/ui/classic/xcbwindow.h @@ -13,6 +13,7 @@ #include #include "fcitx-utils/handlertable.h" #include "fcitx-utils/misc.h" +#include "fcitx-utils/rect.h" #include "window.h" #include "xcb_public.h" #include "xcbui.h" @@ -37,6 +38,14 @@ class XCBWindow : public Window { xcb_window_t wid() const { return wid_; } protected: + virtual double scale() const { return 1.0; } + int logicalFromPhysical(int value) const; + int physicalFromLogical(int value) const; + // X11 window dimensions must be at least one pixel. + int physicalSizeFromLogical(int size) const; + Rect physicalFromLogical(const Rect &rect) const; + static double scaleForDPI(int dpi); + XCBUI *ui_; xcb_window_t wid_ = 0; xcb_colormap_t colorMapNeedFree_ = 0; @@ -44,6 +53,8 @@ class XCBWindow : public Window { std::unique_ptr> eventFilter_; UniqueCPtr surface_; UniqueCPtr contentSurface_; + int physicalWidth_ = 1; + int physicalHeight_ = 1; }; } // namespace fcitx::classicui