diff --git a/meson.build b/meson.build index 0313934db2..33e9e8c4c6 100644 --- a/meson.build +++ b/meson.build @@ -1158,6 +1158,7 @@ if build_tests 'fcitx_status', 'freedesktop_key_file', 'google_client_calendar_list', + 'grid_item_cursor', 'hook_manager', 'hyprland_workspace_backend', 'i18n_language_tag', diff --git a/src/shell/switcher/window_switcher.cpp b/src/shell/switcher/window_switcher.cpp index e9acdff6e7..331f391885 100644 --- a/src/shell/switcher/window_switcher.cpp +++ b/src/shell/switcher/window_switcher.cpp @@ -10,6 +10,7 @@ #include "core/input/keybind_matcher.h" #include "core/log.h" #include "core/ui_phase.h" +#include "cursor-shape-v1-client-protocol.h" #include "i18n/i18n.h" #include "ipc/ipc_service.h" #include "render/animation/animation_manager.h" @@ -1127,6 +1128,7 @@ void WindowSwitcher::buildScene(Instance& instance, std::uint32_t width, std::ui .columnGap = metrics.colGap, .rowGap = metrics.rowGap, .overscanRows = 1, + .itemCursorShape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, .adapter = instance.adapter.get(), .width = metrics.gridW, .height = metrics.gridH, diff --git a/src/ui/builders.cpp b/src/ui/builders.cpp index eb7dae5d37..abe48b885e 100644 --- a/src/ui/builders.cpp +++ b/src/ui/builders.cpp @@ -817,6 +817,9 @@ namespace ui { if (props.overscanRows.has_value()) { control->setOverscanRows(*props.overscanRows); } + if (props.itemCursorShape.has_value()) { + control->setItemCursorShape(*props.itemCursorShape); + } if (props.scrollbarVisible.has_value()) { control->scrollView().setScrollbarVisible(*props.scrollbarVisible); } diff --git a/src/ui/builders.h b/src/ui/builders.h index 00221df9bc..45d519c36c 100644 --- a/src/ui/builders.h +++ b/src/ui/builders.h @@ -433,6 +433,7 @@ namespace ui { std::optional columnGap = std::nullopt; std::optional rowGap = std::nullopt; std::optional overscanRows = std::nullopt; + std::optional itemCursorShape = std::nullopt; std::optional scrollbarVisible = std::nullopt; std::optional scrollCardStyleScale = std::nullopt; VirtualGridAdapter* adapter = nullptr; diff --git a/src/ui/controls/virtual_grid_view.cpp b/src/ui/controls/virtual_grid_view.cpp index c773cc9c32..a45b686231 100644 --- a/src/ui/controls/virtual_grid_view.cpp +++ b/src/ui/controls/virtual_grid_view.cpp @@ -175,6 +175,18 @@ void VirtualGridView::setOverscanRows(std::size_t rows) { markLayoutDirty(); } +void VirtualGridView::setItemCursorShape(std::uint32_t shape) { + if (m_itemCursorShape == shape) { + return; + } + m_itemCursorShape = shape; + for (InputArea* area : m_poolTooltipAreas) { + if (area != nullptr) { + area->setCursorShape(shape); + } + } +} + void VirtualGridView::scrollToIndex(std::size_t index) { m_pendingScrollToIndex = true; m_pendingScrollIndex = index; @@ -349,9 +361,11 @@ void VirtualGridView::doLayout(Renderer& renderer) { m_slotBoundHovered.push_back(false); m_slotBoundOverlayHovered.push_back(false); + // Per-cell overlay: carries the item tooltip and the item cursor shape. auto tooltipArea = std::make_unique(); tooltipArea->setVisible(false); tooltipArea->setAcceptedButtons(0); + tooltipArea->setCursorShape(m_itemCursorShape); tooltipArea->setOnEnter([this, slot](const InputArea::PointerData& data) { onPoolTooltipMotion(slot, data.localX, data.localY); }); diff --git a/src/ui/controls/virtual_grid_view.h b/src/ui/controls/virtual_grid_view.h index 5a2fe2f7f0..34f7d887f7 100644 --- a/src/ui/controls/virtual_grid_view.h +++ b/src/ui/controls/virtual_grid_view.h @@ -5,6 +5,7 @@ #include "ui/controls/flex.h" #include +#include #include #include #include @@ -109,6 +110,8 @@ class VirtualGridView : public Flex { void setColumnGap(float gap); void setRowGap(float gap); void setOverscanRows(std::size_t rows); + // wp_cursor_shape value applied while the pointer is over a cell (0 = inherit). + void setItemCursorShape(std::uint32_t shape); // Content scale, used for the pointer travel threshold that separates a click // from a drag on an adapter-consumed press. void setScale(float scale) { m_scale = scale; } @@ -171,6 +174,7 @@ class VirtualGridView : public Flex { float m_columnGap = 4.0F; float m_rowGap = 4.0F; std::size_t m_overscanRows = 2; + std::uint32_t m_itemCursorShape = 0; float m_scale = 1.0F; std::optional m_selectedIndex; diff --git a/tests/grid_item_cursor_test.cpp b/tests/grid_item_cursor_test.cpp new file mode 100644 index 0000000000..8a3f504959 --- /dev/null +++ b/tests/grid_item_cursor_test.cpp @@ -0,0 +1,99 @@ +#include "render/core/renderer.h" +#include "render/scene/input_dispatcher.h" +#include "render/scene/node.h" +#include "ui/controls/virtual_grid_view.h" + +#include +#include +#include +#include +#include +#include + +namespace { + + class StubRenderer final : public Renderer { + public: + TextMetrics measureText( + std::string_view, float fontSize, FontWeight, float, int, TextAlign, std::string_view, TextEllipsize, bool + ) override { + return TextMetrics{.bottom = fontSize}; + } + + TextMetrics measureFont(float fontSize, FontWeight) override { return TextMetrics{.bottom = fontSize}; } + + void measureTextCursorStops( + std::string_view, float, const std::vector&, std::vector&, FontWeight + ) override {} + + void measureTextCursorStopsWrapped( + std::string_view, float, const std::vector&, float, std::vector&, FontWeight + ) override {} + + TextMetrics measureGlyph(char32_t, float) override { return TextMetrics{.width = 18.0F}; } + + TextureManager& textureManager() override { std::abort(); } + [[nodiscard]] float renderScale() const noexcept override { return 1.0F; } + }; + + class StubAdapter final : public VirtualGridAdapter { + public: + [[nodiscard]] std::size_t itemCount() const override { return 4; } + [[nodiscard]] std::unique_ptr createTile() override { return std::make_unique(); } + void bindTile(Node&, std::size_t, bool, bool) override {} + }; + + bool expect(bool condition, std::string_view what) { + std::println("{}: {}", condition ? "ok" : "FAIL", what); + return condition; + } + +} // namespace + +int main() { + StubRenderer renderer; + StubAdapter adapter; + + VirtualGridView grid; + grid.setFillWidth(false); + grid.setFillHeight(false); + grid.setColumns(2); + grid.setSquareCells(false); + grid.setCellHeight(50.0F); + grid.setColumnGap(20.0F); + grid.setRowGap(20.0F); + grid.setItemCursorShape(21); + grid.setAdapter(&adapter); + grid.setSize(240.0F, 120.0F); + grid.layout(renderer); + + InputDispatcher dispatcher; + std::uint32_t cursor = 0; + dispatcher.setCursorShapeCallback([&cursor](std::uint32_t, std::uint32_t shape) { cursor = shape; }); + dispatcher.setSceneRoot(&grid); + + float cell0X = 0.0F; + float cell0Y = 0.0F; + float cell1X = 0.0F; + float cell1Y = 0.0F; + bool ok = expect(grid.absoluteAnchorForIndex(0, cell0X, cell0Y), "cell 0 anchor resolved"); + ok = expect(grid.absoluteAnchorForIndex(1, cell1X, cell1Y), "cell 1 anchor resolved") && ok; + + dispatcher.pointerEnter(cell0X, cell0Y, 1); + ok = expect(cursor == 21, "item cursor shape applies over a cell") && ok; + + dispatcher.pointerMotion((cell0X + cell1X) * 0.5F, cell0Y, 2); + ok = expect(cursor == 1, "gutter between cells keeps the default cursor") && ok; + + dispatcher.pointerMotion(cell1X, cell1Y, 3); + ok = expect(cursor == 21, "item cursor shape applies to every cell") && ok; + + grid.setItemCursorShape(24); + dispatcher.pointerMotion(cell1X + 1.0F, cell1Y, 4); + ok = expect(cursor == 24, "shape change reaches already-created cell areas") && ok; + + dispatcher.pointerLeave(); + ok = expect(cursor == 1, "leaving the surface restores the default cursor") && ok; + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +}