Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/shell/switcher/window_switcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/ui/builders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/builders.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ namespace ui {
std::optional<float> columnGap = std::nullopt;
std::optional<float> rowGap = std::nullopt;
std::optional<std::size_t> overscanRows = std::nullopt;
std::optional<std::uint32_t> itemCursorShape = std::nullopt;
std::optional<bool> scrollbarVisible = std::nullopt;
std::optional<float> scrollCardStyleScale = std::nullopt;
VirtualGridAdapter* adapter = nullptr;
Expand Down
14 changes: 14 additions & 0 deletions src/ui/controls/virtual_grid_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<InputArea>();
tooltipArea->setVisible(false);
tooltipArea->setAcceptedButtons(0);
tooltipArea->setCursorShape(m_itemCursorShape);
tooltipArea->setOnEnter([this, slot](const InputArea::PointerData& data) {
onPoolTooltipMotion(slot, data.localX, data.localY);
});
Expand Down
4 changes: 4 additions & 0 deletions src/ui/controls/virtual_grid_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ui/controls/flex.h"

#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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<std::size_t> m_selectedIndex;
Expand Down
99 changes: 99 additions & 0 deletions tests/grid_item_cursor_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <cstdlib>
#include <memory>
#include <print>
#include <string_view>
#include <vector>

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::size_t>&, std::vector<float>&, FontWeight
) override {}

void measureTextCursorStopsWrapped(
std::string_view, float, const std::vector<std::size_t>&, float, std::vector<TextCursorStop>&, 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<Node> createTile() override { return std::make_unique<Node>(); }
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;
}