feat(bar): middle clicking on any widget will open its setting

This commit is contained in:
Lemmy
2026-05-08 20:48:24 -04:00
parent 20b6b16ab2
commit 3520001f8d
29 changed files with 236 additions and 22 deletions
+4
View File
@@ -1259,6 +1259,10 @@
"label": "Password Style",
"description": "Visual style for password input masking"
},
"middle-click-opens-widget-settings": {
"label": "Middle Click Opens Widget Settings",
"description": "Open a bar widget's settings from the bar with middle click"
},
"show-location": {
"label": "Show Location",
"description": "Show location name in weather widgets"
+1
View File
@@ -12,6 +12,7 @@ telemetry_enabled = false # send an anonymous usage ping on startup
polkit_agent = false
password_style = "default" # default | random
settings_show_advanced = false # show advanced settings by default in Settings
middle_click_opens_widget_settings = true # middle-click bar widgets to open their Settings entry
show_location = true # hide weather location text in shell UI when false
clipboard_auto_paste = "auto" # off | auto | ctrl_v | ctrl_shift_v | shift_insert
# avatar_path = "~/Pictures/avatar.png"
+6
View File
@@ -909,6 +909,12 @@ void Application::initUi() {
&m_httpClient, &m_weatherService, &m_renderContext, &m_nightLightManager, &m_themeService,
m_bluetoothService.get(), m_brightnessService.get(), kLockKeysEnabled ? &m_lockKeysService : nullptr,
&m_fileWatcher);
m_bar.setOpenWidgetSettingsCallback([this](std::string barName, std::string widgetName) {
if (m_panelManager.isOpen()) {
m_panelManager.closePanel();
}
m_settingsWindow.openToBarWidget(std::move(barName), std::move(widgetName));
});
m_panelManager.setAttachedPanelGeometryCallback(
[this](wl_output* output, std::optional<AttachedPanelGeometry> geometry) {
m_bar.setAttachedPanelGeometry(output, geometry);
+5 -5
View File
@@ -289,11 +289,11 @@ namespace {
a.polkitAgent == b.polkitAgent && a.passwordMaskStyle == b.passwordMaskStyle &&
a.animation.enabled == b.animation.enabled && nearlyEqual(a.animation.speed, b.animation.speed) &&
a.avatarPath == b.avatarPath && a.settingsShowAdvanced == b.settingsShowAdvanced &&
a.showLocation == b.showLocation && a.clipboardAutoPaste == b.clipboardAutoPaste &&
a.shadow.blur == b.shadow.blur && a.shadow.offsetX == b.shadow.offsetX &&
a.shadow.offsetY == b.shadow.offsetY && nearlyEqual(a.shadow.alpha, b.shadow.alpha) &&
a.panel.backgroundBlur == b.panel.backgroundBlur && a.panel.attachLauncher == b.panel.attachLauncher &&
a.panel.attachClipboard == b.panel.attachClipboard &&
a.middleClickOpensWidgetSettings == b.middleClickOpensWidgetSettings && a.showLocation == b.showLocation &&
a.clipboardAutoPaste == b.clipboardAutoPaste && a.shadow.blur == b.shadow.blur &&
a.shadow.offsetX == b.shadow.offsetX && a.shadow.offsetY == b.shadow.offsetY &&
nearlyEqual(a.shadow.alpha, b.shadow.alpha) && a.panel.backgroundBlur == b.panel.backgroundBlur &&
a.panel.attachLauncher == b.panel.attachLauncher && a.panel.attachClipboard == b.panel.attachClipboard &&
a.panel.attachControlCenter == b.panel.attachControlCenter &&
a.panel.attachWallpaper == b.panel.attachWallpaper && a.screenCorners.enabled == b.screenCorners.enabled &&
a.screenCorners.size == b.screenCorners.size && a.mpris.blacklist == b.mpris.blacklist;
+3
View File
@@ -1253,6 +1253,9 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
if (auto v = (*shellTbl)["settings_show_advanced"].value<bool>()) {
shell.settingsShowAdvanced = *v;
}
if (auto v = (*shellTbl)["middle_click_opens_widget_settings"].value<bool>()) {
shell.middleClickOpensWidgetSettings = *v;
}
if (auto v = (*shellTbl)["show_location"].value<bool>()) {
shell.showLocation = *v;
}
+1
View File
@@ -377,6 +377,7 @@ struct ShellConfig {
AnimationConfig animation;
std::string avatarPath;
bool settingsShowAdvanced = false;
bool middleClickOpensWidgetSettings = true;
bool showLocation = true;
ClipboardAutoPasteMode clipboardAutoPaste = ClipboardAutoPasteMode::Auto;
ShadowConfig shadow;
+29
View File
@@ -2,6 +2,13 @@
#include "cursor-shape-v1-client-protocol.h"
namespace {
constexpr std::uint32_t kMouseButtonBase = BTN_MOUSE;
constexpr std::uint32_t kMaxTrackedMouseButtons = 32;
} // namespace
InputArea::InputArea() : Node(NodeType::Base) {}
InputArea::~InputArea() {
@@ -12,6 +19,25 @@ InputArea::~InputArea() {
void InputArea::setDestroyCallback(DestroyCallback callback) { m_destroyCallback = std::move(callback); }
std::uint32_t InputArea::buttonMask(std::uint32_t button) noexcept {
if (button < kMouseButtonBase) {
return 0;
}
const std::uint32_t index = button - kMouseButtonBase;
if (index >= kMaxTrackedMouseButtons) {
return 0;
}
return 1u << index;
}
std::uint32_t InputArea::buttonMask(std::initializer_list<std::uint32_t> buttons) noexcept {
std::uint32_t mask = 0;
for (const auto button : buttons) {
mask |= buttonMask(button);
}
return mask;
}
void InputArea::setOnEnter(PointerCallback callback) { m_onEnter = std::move(callback); }
void InputArea::setOnLeave(VoidCallback callback) { m_onLeave = std::move(callback); }
void InputArea::setOnMotion(PointerCallback callback) { m_onMotion = std::move(callback); }
@@ -32,6 +58,9 @@ void InputArea::setOnClick(PointerCallback callback) {
void InputArea::setCursorShape(std::uint32_t shape) { m_cursorShape = shape; }
void InputArea::setAcceptedButtons(std::uint32_t mask) { m_acceptedButtons = mask; }
bool InputArea::acceptsButton(std::uint32_t button) const noexcept {
return (m_acceptedButtons & buttonMask(button)) != 0;
}
void InputArea::setPropagateEvents(bool propagate) { m_propagateEvents = propagate; }
void InputArea::setEnabled(bool enabled) { m_enabled = enabled; }
void InputArea::setFocusable(bool focusable) { m_focusable = focusable; }
+6 -1
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <linux/input-event-codes.h>
class InputArea : public Node {
@@ -45,6 +46,9 @@ public:
InputArea();
~InputArea() override;
[[nodiscard]] static std::uint32_t buttonMask(std::uint32_t button) noexcept;
[[nodiscard]] static std::uint32_t buttonMask(std::initializer_list<std::uint32_t> buttons) noexcept;
// InputArea is a transparent hit-test wrapper with no layout semantics of its
// own; its internal layout hook forwards to visible children so callers can
// use it as a clickable container without manually re-laying children.
@@ -73,6 +77,7 @@ public:
void setAcceptedButtons(std::uint32_t mask);
[[nodiscard]] std::uint32_t acceptedButtons() const noexcept { return m_acceptedButtons; }
[[nodiscard]] bool acceptsButton(std::uint32_t button) const noexcept;
void setPropagateEvents(bool propagate);
[[nodiscard]] bool propagateEvents() const noexcept { return m_propagateEvents; }
@@ -114,7 +119,7 @@ private:
VoidCallback m_onFocusLoss;
std::uint32_t m_cursorShape = 0;
std::uint32_t m_acceptedButtons = BTN_LEFT;
std::uint32_t m_acceptedButtons = buttonMask(BTN_LEFT);
bool m_propagateEvents = false;
bool m_enabled = true;
bool m_hovered = false;
+13 -2
View File
@@ -17,6 +17,16 @@ namespace {
return false;
}
InputArea* inputAreaAcceptingButton(InputArea* area, std::uint32_t button) {
for (Node* node = area; node != nullptr; node = node->parent()) {
auto* candidate = dynamic_cast<InputArea*>(node);
if (candidate != nullptr && candidate->enabled() && candidate->acceptsButton(button)) {
return candidate;
}
}
return nullptr;
}
} // namespace
void InputDispatcher::setSceneRoot(Node* root) {
@@ -77,12 +87,12 @@ bool InputDispatcher::pointerButton(float x, float y, std::uint32_t button, bool
pruneDetachedAreas();
InputArea* target = m_capturedArea != nullptr ? m_capturedArea : m_hoveredArea;
InputArea* target = m_capturedArea != nullptr ? m_capturedArea : inputAreaAcceptingButton(m_hoveredArea, button);
// Press with no hover target: subtree may have been rebuilt (same global coords, new InputArea*).
if (target == nullptr && m_capturedArea == nullptr && pressed && m_hasPointerPosition) {
updateHover(x, y, m_lastSerial);
target = m_hoveredArea;
target = inputAreaAcceptingButton(m_hoveredArea, button);
}
if (target != nullptr) {
@@ -92,6 +102,7 @@ bool InputDispatcher::pointerButton(float x, float y, std::uint32_t button, bool
target->dispatchPress(localX, localY, button, pressed);
if (pressed) {
m_capturedArea = target;
trackArea(target);
if (target->focusable()) {
setFocus(target);
}
+48
View File
@@ -45,6 +45,40 @@ namespace {
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
}
Widget* widgetAtPoint(const std::vector<std::unique_ptr<Widget>>& widgets, float sceneX, float sceneY) {
for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) {
auto* widget = it->get();
if (widget == nullptr || widget->root() == nullptr || !widget->root()->visible()) {
continue;
}
if (pointInsideNode(widget->root(), sceneX, sceneY)) {
return widget;
}
}
for (auto it = widgets.rbegin(); it != widgets.rend(); ++it) {
auto* widget = it->get();
auto* root = widget != nullptr ? widget->root() : nullptr;
auto* bounds = widget != nullptr ? widget->layoutBoundsNode() : nullptr;
if (root == nullptr || bounds == nullptr || bounds == root || root->parent() != bounds || !bounds->visible()) {
continue;
}
if (pointInsideNode(bounds, sceneX, sceneY)) {
return widget;
}
}
return nullptr;
}
Widget* widgetAtPoint(const BarInstance& instance, float sceneX, float sceneY) {
if (auto* widget = widgetAtPoint(instance.endWidgets, sceneX, sceneY); widget != nullptr) {
return widget;
}
if (auto* widget = widgetAtPoint(instance.centerWidgets, sceneX, sceneY); widget != nullptr) {
return widget;
}
return widgetAtPoint(instance.startWidgets, sceneX, sceneY);
}
std::pair<float, float> surfaceOriginForOutputLocal(const BarInstance& instance, const WaylandOutput& outputInfo) {
if (instance.surface == nullptr) {
return {0.0f, 0.0f};
@@ -647,6 +681,10 @@ void Bar::setAutoHideSuppressionCallback(std::function<bool()> callback) {
m_autoHideSuppressionCallback = std::move(callback);
}
void Bar::setOpenWidgetSettingsCallback(std::function<void(std::string, std::string)> callback) {
m_openWidgetSettingsCallback = std::move(callback);
}
bool Bar::isRunning() const noexcept {
if (m_forceHidden) {
return true; // hidden but still alive — do not exit the main loop
@@ -984,6 +1022,7 @@ void Bar::populateWidgets(BarInstance& instance) {
auto widget =
m_widgetFactory->create(name, instance.output, instance.barConfig.scale, instance.barConfig.position);
if (widget != nullptr) {
widget->setConfigName(name);
const WidgetConfig* wcPtr = nullptr;
if (auto it = widgetConfigs.find(name); it != widgetConfigs.end()) {
wcPtr = &it->second;
@@ -1674,6 +1713,15 @@ bool Bar::onPointerEvent(const PointerEvent& event) {
}
}
if (targetInstance != nullptr && event.type == PointerEvent::Type::Button && event.button == BTN_MIDDLE &&
event.state == 1 && m_config != nullptr && m_config->config().shell.middleClickOpensWidgetSettings) {
auto* widget = widgetAtPoint(*targetInstance, static_cast<float>(event.sx), static_cast<float>(event.sy));
if (widget != nullptr && !widget->configName().empty() && m_openWidgetSettingsCallback) {
m_openWidgetSettingsCallback(targetInstance->barConfig.name, std::string(widget->configName()));
return true;
}
}
if (targetInstance != nullptr && targetInstance->attachedPopupCount > 0) {
switch (event.type) {
case PointerEvent::Type::Enter:
+2
View File
@@ -63,6 +63,7 @@ public:
void refresh();
void requestLayout();
void setAutoHideSuppressionCallback(std::function<bool()> callback);
void setOpenWidgetSettingsCallback(std::function<void(std::string, std::string)> callback);
// Requests a redraw on every bar surface without re-running widget update/layout.
// Intended for reactive restyling (palette changes) where the scene graph has
// already been mutated in place and only a repaint is needed.
@@ -139,4 +140,5 @@ private:
std::unordered_map<wl_surface*, BarInstance*> m_surfaceMap;
BarInstance* m_hoveredInstance = nullptr;
std::function<bool()> m_autoHideSuppressionCallback;
std::function<void(std::string, std::string)> m_openWidgetSettingsCallback;
};
+5
View File
@@ -7,7 +7,9 @@
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
class AnimationManager;
class Box;
@@ -54,6 +56,8 @@ public:
void setPanelToggleCallback(PanelToggleCallback callback);
void setContentScale(float scale) noexcept { m_contentScale = scale; }
[[nodiscard]] float contentScale() const noexcept { return m_contentScale; }
void setConfigName(std::string name) { m_configName = std::move(name); }
[[nodiscard]] std::string_view configName() const noexcept { return m_configName; }
void setAnchor(bool anchor) noexcept { m_anchor = anchor; }
[[nodiscard]] bool isAnchor() const noexcept { return m_anchor; }
@@ -86,6 +90,7 @@ protected:
virtual void doUpdate(Renderer& renderer) { (void)renderer; }
float m_contentScale = 1.0f;
std::string m_configName;
bool m_anchor = false;
AnimationManager* m_animations = nullptr;
UpdateCallback m_updateCallback;
+1 -1
View File
@@ -33,7 +33,7 @@ MediaWidget::MediaWidget(MprisService* mpris, HttpClient* httpClient, wl_output*
void MediaWidget::create() {
auto area = std::make_unique<InputArea>();
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
area->setOnEnter([this](const InputArea::PointerData&) {
applyTitleScrollMode(m_label != nullptr && m_label->visible());
this->requestUpdate();
+1 -1
View File
@@ -25,7 +25,7 @@ NightLightWidget::NightLightWidget(NightLightManager* nightLight) : m_nightLight
void NightLightWidget::create() {
auto area = std::make_unique<InputArea>();
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
area->setOnClick([this](const InputArea::PointerData& data) {
if (m_nightLight == nullptr) {
return;
@@ -20,6 +20,7 @@ NotificationWidget::NotificationWidget(NotificationManager* manager, wl_output*
void NotificationWidget::create() {
auto area = std::make_unique<InputArea>();
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
area->setOnClick([this](const InputArea::PointerData& data) {
if (data.button == BTN_RIGHT) {
if (m_manager != nullptr) {
+1 -1
View File
@@ -61,7 +61,7 @@ void ScriptedWidget::create() {
m_host = std::make_unique<LuauHost>();
auto area = std::make_unique<InputArea>();
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT | BTN_MIDDLE);
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT, BTN_MIDDLE}));
area->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
area->setOnClick([this](const InputArea::PointerData& data) {
if (!m_host)
+3 -3
View File
@@ -162,7 +162,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
auto createTaskTile = [&](const TaskModel& task) {
auto area = std::make_unique<InputArea>();
area->setFrameSize(tileSize, tileSize);
area->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
area->setOnAxisHandler(workspaceAxisHandler);
const WorkspaceModel* taskWorkspace = nullptr;
@@ -276,7 +276,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
auto switcher = std::make_unique<InputArea>();
switcher->setFrameSize(groupWidth, groupHeight);
switcher->setPosition(0.0f, 0.0f);
switcher->setAcceptedButtons(BTN_LEFT);
switcher->setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
switcher->setOnAxisHandler(workspaceAxisHandler);
auto wsCopy = ws.workspace;
switcher->setOnClick([this, wsCopy](const InputArea::PointerData& data) {
@@ -303,7 +303,7 @@ void TaskbarWidget::buildTaskButtons(Renderer& renderer) {
auto badgeHit = std::make_unique<InputArea>();
badgeHit->setFrameSize(badgeWidth, badgeBase);
badgeHit->setPosition(badgeLeft, badgeTop);
badgeHit->setAcceptedButtons(BTN_LEFT);
badgeHit->setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
badgeHit->setOnAxisHandler(workspaceAxisHandler);
auto wsForBadge = ws.workspace;
badgeHit->setOnClick([this, wsForBadge](const InputArea::PointerData& data) {
+1 -2
View File
@@ -616,6 +616,7 @@ void TrayWidget::rebuild(Renderer& renderer) {
area->setSize(itemSize, itemSize);
iconNode->setPosition(std::round((itemSize - iconW) * 0.5f), std::round((itemSize - iconH) * 0.5f));
auto itemId = item.id;
area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
area->setOnClick([this, itemId](const InputArea::PointerData& data) {
if (m_tray == nullptr) {
return;
@@ -627,8 +628,6 @@ void TrayWidget::rebuild(Renderer& renderer) {
}
} else if (data.button == BTN_RIGHT) {
m_tray->requestMenuToggle(itemId);
} else if (data.button == BTN_MIDDLE) {
(void)m_tray->openContextMenu(itemId);
}
});
area->addChild(std::move(iconNode));
+1 -1
View File
@@ -1068,7 +1068,7 @@ void Dock::rebuildItems(DockInstance& instance) {
instPtr->sceneRoot->markPaintDirty();
}
});
areaNode->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
areaNode->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
areaNode->setOnClick([itemPtr, instPtr, this](const InputArea::PointerData& d) {
if (d.button == BTN_LEFT) {
handleItemClick(*instPtr, *itemPtr);
@@ -1484,7 +1484,7 @@ InputArea* NotificationToast::buildCard(const PopupEntry& entry, Node** outCardC
auto viewport = std::make_unique<InputArea>();
viewport->setSize(kCardWidth, cardHeight);
viewport->setClipChildren(true);
viewport->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
viewport->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
// Right-clicking anywhere dismisses the card, while the visual (X) keeps its
// familiar left-click close affordance without adding a nested hover target.
viewport->setOnClick([this, id = entry.notificationId](const InputArea::PointerData& data) {
+3
View File
@@ -1040,6 +1040,9 @@ namespace settings {
const bool hasEdit = !ctx.editingWidgetName.empty();
auto inspector = std::make_unique<Flex>();
if (ctx.setScrollTarget) {
ctx.setScrollTarget(inspector.get());
}
inspector->setDirection(FlexDirection::Vertical);
inspector->setAlign(FlexAlign::Stretch);
inspector->setGap(Style::spaceSm * ctx.scale);
+1
View File
@@ -30,6 +30,7 @@ namespace settings {
std::function<void()> requestRebuild;
std::function<void()> resetContentScroll;
std::function<void(Node*)> setScrollTarget;
std::function<void(InputArea*)> focusArea;
std::function<void(const std::vector<std::string>&, Button*)> openWidgetAddPopup;
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
+1
View File
@@ -1234,6 +1234,7 @@ namespace settings {
.creatingWidgetType = ctx.creatingWidgetType,
.requestRebuild = ctx.requestRebuild,
.resetContentScroll = ctx.resetContentScroll,
.setScrollTarget = ctx.setScrollTarget,
.focusArea = ctx.focusArea,
.openWidgetAddPopup = ctx.openBarWidgetAddPopup,
.setOverride = ctx.setOverride,
+2
View File
@@ -13,6 +13,7 @@
class Flex;
class InputArea;
class Button;
class Node;
namespace settings {
@@ -38,6 +39,7 @@ namespace settings {
std::function<void()> requestRebuild;
std::function<void()> requestContentRebuild;
std::function<void()> resetContentScroll;
std::function<void(Node*)> setScrollTarget;
std::function<void(InputArea*)> focusArea;
std::function<void(const std::vector<std::string>&, Button*)> openBarWidgetAddPopup;
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
+5
View File
@@ -588,6 +588,11 @@ namespace settings {
tr("settings.schema.shell.password-style.description"), {"shell", "password_style"},
asSegmented(enumSelect(kPasswordMaskStyles, cfg.shell.passwordMaskStyle)),
"polkit lock mask"));
entries.push_back(makeEntry(
"shell", "behavior", tr("settings.schema.shell.middle-click-opens-widget-settings.label"),
tr("settings.schema.shell.middle-click-opens-widget-settings.description"),
{"shell", "middle_click_opens_widget_settings"}, ToggleSetting{cfg.shell.middleClickOpensWidgetSettings},
"bar widget settings middle click configure"));
entries.push_back(makeEntry("shell", "location", tr("settings.schema.shell.show-location.label"),
tr("settings.schema.shell.show-location.description"), {"shell", "show_location"},
ToggleSetting{cfg.shell.showLocation}, "weather"));
+80
View File
@@ -254,6 +254,26 @@ void SettingsWindow::open() {
m_lastSceneHeight = 0;
}
void SettingsWindow::openToBarWidget(std::string barName, std::string widgetName) {
clearTransientSettingsState();
clearStatusMessage();
m_searchQuery.clear();
m_selectedSection = "bar";
m_selectedBarName = std::move(barName);
m_selectedMonitorOverride.clear();
m_editingWidgetName = std::move(widgetName);
m_contentScrollState.offset = 0.0f;
m_scrollToPendingContentTarget = true;
m_pendingContentScrollTarget = nullptr;
m_sidebarScrollState.offset = 0.0f;
const bool wasOpen = isOpen();
open();
if (wasOpen && isOpen()) {
requestSceneRebuild();
}
}
void SettingsWindow::close() {
if (!isOpen()) {
return;
@@ -268,6 +288,7 @@ void SettingsWindow::destroyWindow() {
}
m_mainContainer = nullptr;
m_contentContainer = nullptr;
m_contentScrollView = nullptr;
m_actionsMenuButton = nullptr;
if (m_actionsMenuPopup != nullptr) {
m_actionsMenuPopup->close();
@@ -287,6 +308,8 @@ void SettingsWindow::destroyWindow() {
m_rebuildRequested = false;
m_contentRebuildRequested = false;
m_focusSearchOnRebuild = false;
m_scrollToPendingContentTarget = false;
m_pendingContentScrollTarget = nullptr;
m_statusMessage.clear();
m_statusIsError = false;
m_creatingBarName.clear();
@@ -347,6 +370,7 @@ void SettingsWindow::prepareFrame(bool /*needsUpdate*/, bool needsLayout) {
m_contentRebuildRequested = false;
}
m_sceneRoot->layout(*m_renderContext);
applyPendingContentScrollTarget(Style::spaceMd * uiScale());
m_lastSceneWidth = width;
m_lastSceneHeight = height;
}
@@ -377,6 +401,57 @@ void SettingsWindow::requestContentRebuild() {
});
}
void SettingsWindow::applyPendingContentScrollTarget(float margin) {
if (!m_scrollToPendingContentTarget) {
return;
}
auto clearPending = [this]() {
m_scrollToPendingContentTarget = false;
m_pendingContentScrollTarget = nullptr;
};
if (m_contentScrollView == nullptr || m_contentScrollView->content() == nullptr ||
m_pendingContentScrollTarget == nullptr) {
clearPending();
return;
}
const float viewportHeight =
std::max(0.0f, m_contentScrollView->height() - m_contentScrollView->viewportPaddingV() * 2.0f);
if (viewportHeight <= 0.0f) {
clearPending();
return;
}
float targetX = 0.0f;
float targetY = 0.0f;
float contentX = 0.0f;
float contentY = 0.0f;
Node::absolutePosition(m_pendingContentScrollTarget, targetX, targetY);
Node::absolutePosition(m_contentScrollView->content(), contentX, contentY);
(void)targetX;
(void)contentX;
const float targetTop = std::max(0.0f, targetY - contentY - margin);
const float targetBottom = targetY - contentY + m_pendingContentScrollTarget->height() + margin;
const float currentTop = m_contentScrollView->scrollOffset();
const float currentBottom = currentTop + viewportHeight;
float desiredOffset = currentTop;
if (targetBottom - targetTop >= viewportHeight) {
desiredOffset = targetTop;
} else if (targetTop < currentTop) {
desiredOffset = targetTop;
} else if (targetBottom > currentBottom) {
desiredOffset = targetBottom - viewportHeight;
}
m_contentScrollView->setScrollOffset(desiredOffset);
m_contentScrollState.offset = m_contentScrollView->scrollOffset();
clearPending();
}
void SettingsWindow::clearStatusMessage() {
m_statusMessage.clear();
m_statusIsError = false;
@@ -945,6 +1020,7 @@ void SettingsWindow::rebuildSettingsContent() {
return;
}
m_pendingContentScrollTarget = nullptr;
while (!m_contentContainer->children().empty()) {
m_contentContainer->removeChild(m_contentContainer->children().back().get());
}
@@ -1030,6 +1106,7 @@ void SettingsWindow::rebuildSettingsContent() {
.requestRebuild = requestRebuild,
.requestContentRebuild = requestContent,
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
.setScrollTarget = [this](Node* target) { m_pendingContentScrollTarget = target; },
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
.openBarWidgetAddPopup = [this](const std::vector<std::string>& lanePath,
Button* anchorButton) { openBarWidgetAddPopup(lanePath, anchorButton); },
@@ -1050,6 +1127,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
const float h = static_cast<float>(height);
const float scale = uiScale();
m_actionsMenuButton = nullptr;
m_contentScrollView = nullptr;
const Config fallbackCfg{};
const Config& cfg = m_config != nullptr ? m_config->config() : fallbackCfg;
const auto availableBars = settings::barNames(cfg);
@@ -1396,6 +1474,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
scroll->setViewportPaddingV(Style::spaceSm * scale);
scroll->clearFill();
scroll->clearBorder();
m_contentScrollView = scroll.get();
auto* content = scroll->content();
m_contentContainer = content;
content->setDirection(FlexDirection::Vertical);
@@ -1415,6 +1494,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) {
main->setSize(w, h);
main->layout(*m_renderContext);
applyPendingContentScrollTarget(Style::spaceMd * scale);
m_mainContainer = static_cast<Flex*>(m_sceneRoot->addChild(std::move(main)));
m_inputDispatcher.setSceneRoot(m_sceneRoot.get());
+5
View File
@@ -36,6 +36,7 @@ public:
DependencyService* dependencies);
void open();
void openToBarWidget(std::string barName, std::string widgetName);
void close();
[[nodiscard]] bool isOpen() const noexcept { return m_surface != nullptr && m_surface->isRunning(); }
[[nodiscard]] wl_surface* wlSurface() const noexcept {
@@ -57,6 +58,7 @@ private:
void rebuildSettingsContent();
void requestSceneRebuild();
void requestContentRebuild();
void applyPendingContentScrollTarget(float margin);
void clearStatusMessage();
void clearTransientSettingsState();
void openActionsMenu();
@@ -89,6 +91,7 @@ private:
Box* m_panelBackground = nullptr; // Window-sized background panel inside m_sceneRoot
Button* m_actionsMenuButton = nullptr;
Flex* m_contentContainer = nullptr;
ScrollView* m_contentScrollView = nullptr;
std::unique_ptr<ContextMenuPopup> m_actionsMenuPopup;
std::unique_ptr<settings::WidgetAddPopup> m_widgetAddPopup;
InputDispatcher m_inputDispatcher;
@@ -104,6 +107,8 @@ private:
bool m_rebuildRequested = false;
bool m_contentRebuildRequested = false;
bool m_focusSearchOnRebuild = false;
bool m_scrollToPendingContentTarget = false;
Node* m_pendingContentScrollTarget = nullptr;
std::string m_searchQuery;
std::string m_openWidgetPickerPath;
std::string m_openSearchPickerPath;
+1 -1
View File
@@ -16,7 +16,7 @@
WallpaperTile::WallpaperTile(float cellWidth, float cellHeight, float contentScale)
: m_cellWidth(cellWidth), m_cellHeight(cellHeight), m_contentScale(contentScale) {
setAcceptedButtons(BTN_LEFT);
setAcceptedButtons(InputArea::buttonMask(BTN_LEFT));
setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
setOnClick([this](const InputArea::PointerData&) {
if (m_hasEntry && m_onClick) {
+5 -3
View File
@@ -168,7 +168,7 @@ Button::Button() {
}
if (data.button == BTN_RIGHT && m_onRightClick) {
m_onRightClick();
} else if (m_onClick) {
} else if (data.button == BTN_LEFT && m_onClick) {
m_onClick();
}
});
@@ -228,7 +228,8 @@ void Button::setOnClick(std::function<void()> callback) {
void Button::setOnRightClick(std::function<void()> callback) {
m_onRightClick = std::move(callback);
if (m_inputArea != nullptr) {
m_inputArea->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
m_inputArea->setAcceptedButtons(m_onRightClick ? InputArea::buttonMask({BTN_LEFT, BTN_RIGHT})
: InputArea::buttonMask(BTN_LEFT));
}
refreshInputAreaEnabled();
}
@@ -329,7 +330,8 @@ void Button::refreshInputAreaEnabled() {
if (m_inputArea != nullptr) {
m_inputArea->setEnabled(m_enabled && (static_cast<bool>(m_onClick) || static_cast<bool>(m_onMotion) ||
static_cast<bool>(m_onPointerMotion) || static_cast<bool>(m_onPress) ||
static_cast<bool>(m_onEnter) || static_cast<bool>(m_onLeave)));
static_cast<bool>(m_onEnter) || static_cast<bool>(m_onLeave) ||
static_cast<bool>(m_onRightClick)));
}
}