diff --git a/docs/user/keybinds.md b/docs/user/keybinds.md index 98d5f5a..a495938 100644 --- a/docs/user/keybinds.md +++ b/docs/user/keybinds.md @@ -102,6 +102,7 @@ These take no argument. | `window-consume-left` | Pull the focused window into the column to its left. | | `window-expel-right` | Pop the focused window out of its column into a new column to the right. | | `window-cycle-width` | Cycle the focused column through its preset widths. | +| `window-cycle-width-back` | Cycle the focused column through its preset widths in reverse. | | `window-toggle-fullscreen` | Toggle fullscreen for the focused window. | | `window-toggle-maximize` | Toggle the focused column's full-width state. | | `window-toggle-maximize-to-edges` | Toggle maximization of the focused window to the usable area's edges, without gaps or borders. Layer-shell exclusive zones remain visible. | diff --git a/src/config/keybind_parse.cpp b/src/config/keybind_parse.cpp index 5051313..a26e3fd 100644 --- a/src/config/keybind_parse.cpp +++ b/src/config/keybind_parse.cpp @@ -177,6 +177,7 @@ namespace umbriel { {"window-close", "[]", KeybindAction::WindowClose, ActionArgKind::OptionalWindowId}, {"window-consume-left", "", KeybindAction::WindowConsumeLeft}, {"window-cycle-width", "", KeybindAction::WindowCycleWidth}, + {"window-cycle-width-back", "", KeybindAction::WindowCycleWidthBack}, {"window-expel-right", "", KeybindAction::WindowExpelRight}, {"window-focus", "", KeybindAction::WindowFocusId, ActionArgKind::WindowId}, {"window-focus-down", "", KeybindAction::WindowFocusDown}, @@ -459,6 +460,7 @@ namespace umbriel { add(KeybindAction::WindowConsumeLeft, XKB_KEY_comma); add(KeybindAction::WindowExpelRight, XKB_KEY_period); add(KeybindAction::WindowCycleWidth, XKB_KEY_r); + add(KeybindAction::WindowCycleWidthBack, XKB_KEY_r, WLR_MODIFIER_SHIFT); add(KeybindAction::ToggleFullscreen, XKB_KEY_f); add(KeybindAction::ToggleMaximize, XKB_KEY_f, WLR_MODIFIER_CTRL); add(KeybindAction::ToggleMaximizeToEdges, XKB_KEY_m); diff --git a/src/config/keybind_parse.h b/src/config/keybind_parse.h index 727f8e1..6111282 100644 --- a/src/config/keybind_parse.h +++ b/src/config/keybind_parse.h @@ -38,6 +38,7 @@ namespace umbriel { WindowConsumeLeft, WindowExpelRight, WindowCycleWidth, + WindowCycleWidthBack, WindowSetWidth, ToggleMaximize, ToggleMaximizeToEdges, diff --git a/src/layout/dwindle.cpp b/src/layout/dwindle.cpp index 5cc5bfc..d610f3e 100644 --- a/src/layout/dwindle.cpp +++ b/src/layout/dwindle.cpp @@ -494,7 +494,7 @@ namespace umbriel { return verticalSibling(view, direction); } - bool DwindleLayout::cycleWidth(int columnIndex) { + bool DwindleLayout::cycleWidth(int columnIndex, int direction) { Node* node = nodeAtFlatIndex(columnIndex); const std::vector splits = widthSplits(node); if (splits.empty()) { @@ -505,8 +505,16 @@ namespace umbriel { current *= widthShare(split); } const auto& presets = m_config->widthPresets; - const auto it = std::ranges::find_if(presets, [current](double preset) { return preset > current + 0.0001; }); - const double next = (it == presets.end()) ? presets[0] : *it; + double next = 0.0; + if (direction < 0) { + const auto it = std::ranges::find_if(presets | std::views::reverse, [current](double preset) { + return preset < current - 0.0001; + }); + next = it == presets.rend() ? presets.back() : *it; + } else { + const auto it = std::ranges::find_if(presets, [current](double preset) { return preset > current + 0.0001; }); + next = it == presets.end() ? presets[0] : *it; + } return applyWidthFraction(splits, next); } diff --git a/src/layout/dwindle.h b/src/layout/dwindle.h index 6bf75a3..67abcdd 100644 --- a/src/layout/dwindle.h +++ b/src/layout/dwindle.h @@ -66,7 +66,7 @@ namespace umbriel { [[nodiscard]] uint32_t sanitizeResizeEdges(const View* view, uint32_t edges) const override; std::unique_ptr beginResize(View* view, uint32_t edges, const wlr_box& usable) override; - bool cycleWidth(int columnIndex) override; + bool cycleWidth(int columnIndex, int direction) override; bool toggleFullWidth(int columnIndex) override; [[nodiscard]] bool isFullWidth(int columnIndex) const override; bool setWidthFraction(int columnIndex, double fraction) override; diff --git a/src/layout/layout.h b/src/layout/layout.h index dd2671c..b0895ba 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -114,7 +114,7 @@ namespace umbriel { [[nodiscard]] virtual View* focusVerticalLeaf(const View* /*view*/, int /*direction*/) const { return nullptr; } - virtual bool cycleWidth(int columnIndex) = 0; + virtual bool cycleWidth(int columnIndex, int direction) = 0; virtual bool toggleFullWidth(int columnIndex) = 0; virtual bool setWidthFraction(int columnIndex, double fraction) = 0; virtual void clearFullWidthState(int columnIndex) = 0; diff --git a/src/layout/scrolling.cpp b/src/layout/scrolling.cpp index 0a7c463..dd25b7c 100644 --- a/src/layout/scrolling.cpp +++ b/src/layout/scrolling.cpp @@ -4,6 +4,7 @@ #include #include +#include // wlr_box and WLR_EDGE_* only. Layout geometry must not pull src/wlr.h, which // drags SceneFX and the renderer into a translation unit that does arithmetic. @@ -440,16 +441,22 @@ namespace umbriel { return {.x = it->x, .y = it->y, .width = it->width, .height = it->height}; } - bool ScrollingLayout::cycleWidth(int columnIndex) { + bool ScrollingLayout::cycleWidth(int columnIndex, int direction) { if (columnIndex < 0 || columnIndex >= static_cast(m_columns.size())) { return false; } Column& column = m_columns[static_cast(columnIndex)]; const auto& presets = m_config->widthPresets; - const auto it = std::ranges::find_if(presets, [current = column.widthFrac](double preset) { - return preset > current + 0.0001; - }); - column.widthFrac = it == presets.end() ? presets[0] : *it; + const double current = column.widthFrac; + if (direction < 0) { + const auto it = std::ranges::find_if(presets | std::views::reverse, [current](double preset) { + return preset < current - 0.0001; + }); + column.widthFrac = it == presets.rend() ? presets.back() : *it; + } else { + const auto it = std::ranges::find_if(presets, [current](double preset) { return preset > current + 0.0001; }); + column.widthFrac = it == presets.end() ? presets[0] : *it; + } column.savedWidthFrac = 0.0; return true; } diff --git a/src/layout/scrolling.h b/src/layout/scrolling.h index 7419ac5..955f7de 100644 --- a/src/layout/scrolling.h +++ b/src/layout/scrolling.h @@ -49,7 +49,7 @@ namespace umbriel { [[nodiscard]] InitialSize initialSize(const wlr_box& usable, std::optional ruleWidthFraction) const override; - bool cycleWidth(int columnIndex) override; + bool cycleWidth(int columnIndex, int direction) override; bool toggleFullWidth(int columnIndex) override; bool setWidthFraction(int columnIndex, double fraction) override; void clearFullWidthState(int columnIndex) override; diff --git a/src/scene/cheatsheet_rows.cpp b/src/scene/cheatsheet_rows.cpp index 7e081be..4a6f052 100644 --- a/src/scene/cheatsheet_rows.cpp +++ b/src/scene/cheatsheet_rows.cpp @@ -280,6 +280,7 @@ namespace { case A::WindowConsumeLeft: case A::WindowExpelRight: case A::WindowCycleWidth: + case A::WindowCycleWidthBack: case A::WindowSetWidth: case A::WindowModifyWidth: case A::WindowCenter: diff --git a/src/server/actions.cpp b/src/server/actions.cpp index 17148c8..94de4d5 100644 --- a/src/server/actions.cpp +++ b/src/server/actions.cpp @@ -406,9 +406,9 @@ namespace umbriel { return true; } - bool actionCycleWidth(Server& server, const Keybind& /*bind*/, std::string* /*error*/) { + template bool actionCycleWidth(Server& server, const Keybind& /*bind*/, std::string* /*error*/) { if (Workspace* workspace = activeWorkspace(server)) { - workspace->cycleFocusedWidth(); + workspace->cycleFocusedWidth(Direction); } return true; } @@ -845,7 +845,8 @@ namespace umbriel { &actionMoveVertical<1>, &actionConsumeLeft, &actionExpelRight, - &actionCycleWidth, + &actionCycleWidth<1>, + &actionCycleWidth<-1>, &actionSetWidth, &actionToggleMaximize, &actionToggleMaximizeToEdges, diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 3660395..0038d48 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -638,12 +638,12 @@ namespace umbriel { return true; } - bool Workspace::cycleFocusedWidth() { + bool Workspace::cycleFocusedWidth(int direction) { if (m_focusedView != nullptr && m_focusedView->maximizedToEdges()) { m_focusedView->setMaximizedToEdges(false); } const int column = m_layout->columnOf(m_focusedView); - if (!m_layout->cycleWidth(column)) { + if (!m_layout->cycleWidth(column, direction)) { return false; } wlr_xdg_toplevel_set_maximized(m_focusedView->toplevel(), false); diff --git a/src/workspace/workspace.h b/src/workspace/workspace.h index 9ff7641..612fa76 100644 --- a/src/workspace/workspace.h +++ b/src/workspace/workspace.h @@ -92,7 +92,7 @@ namespace umbriel { bool consumeFocusedLeft(); bool expelFocusedRight(); bool moveFocusedVertical(int direction); - bool cycleFocusedWidth(); + bool cycleFocusedWidth(int direction); bool setFocusedWidth(double fraction); // Incremental width change: apply `delta` to the focused column's current // width fraction, clamped to [0.1, 1.0]. diff --git a/tests/dwindle_layout.cpp b/tests/dwindle_layout.cpp index beb53c1..27f1961 100644 --- a/tests/dwindle_layout.cpp +++ b/tests/dwindle_layout.cpp @@ -354,6 +354,30 @@ UMBRIEL_TEST(resizeBoundaryRejectsAScreenFacingEdge) { CHECK(!fixture.layout.setResizeBoundary(stub(0), WLR_EDGE_LEFT, 0.75)); } +UMBRIEL_TEST(cycleWidthBackWalksThePresetsInReverse) { + Fixture fixture; + fixture.config.widthPresets = {1.0 / 3.0, 0.5, 2.0 / 3.0}; + fixture.addLeaves(2); + fixture.layout.arrange(kUsable); + + const int span = kUsable.width - 2 * fixture.config.edgePad - fixture.config.totalGap; + + CHECK(fixture.layout.setWidthFraction(0, 2.0 / 3.0)); + + CHECK(fixture.layout.cycleWidth(0, -1)); + fixture.layout.arrange(kUsable); + CHECK(std::abs(fixture.layout.targetBox(stub(0)).width - static_cast(span * 0.5)) <= 2); + + CHECK(fixture.layout.cycleWidth(0, -1)); + fixture.layout.arrange(kUsable); + CHECK(std::abs(fixture.layout.targetBox(stub(0)).width - static_cast(span * (1.0 / 3.0))) <= 2); + + // Wraps back to the last preset. + CHECK(fixture.layout.cycleWidth(0, -1)); + fixture.layout.arrange(kUsable); + CHECK(std::abs(fixture.layout.targetBox(stub(0)).width - static_cast(span * (2.0 / 3.0))) <= 2); +} + // The scrolling-only API is no longer reachable from a DwindleLayout at all: scroll offsets, column positions, and row // weights moved onto ScrollingLayout, so the question a previous test asked here ("does dwindle answer 0?") cannot be // compiled any more. That is the point. Callers reach those through Workspace::scrollingLayout(), which is null for a diff --git a/tests/scrolling_layout.cpp b/tests/scrolling_layout.cpp index 17b4a3e..655f9ae 100644 --- a/tests/scrolling_layout.cpp +++ b/tests/scrolling_layout.cpp @@ -296,18 +296,34 @@ UMBRIEL_TEST(cycleWidthWalksThePresets) { fixture.addColumns(1); CHECK(fixture.layout.setWidthFraction(0, 1.0 / 3)); - CHECK(fixture.layout.cycleWidth(0)); + CHECK(fixture.layout.cycleWidth(0, 1)); const double second = fixture.layout.widthFraction(0); CHECK(std::fabs(second - 0.5) < 1e-6); - CHECK(fixture.layout.cycleWidth(0)); + CHECK(fixture.layout.cycleWidth(0, 1)); CHECK(std::fabs(fixture.layout.widthFraction(0) - 2.0 / 3) < 1e-6); // Wraps back to the first preset. - CHECK(fixture.layout.cycleWidth(0)); + CHECK(fixture.layout.cycleWidth(0, 1)); CHECK(std::fabs(fixture.layout.widthFraction(0) - 1.0 / 3) < 1e-6); } +UMBRIEL_TEST(cycleWidthBackWalksThePresetsInReverse) { + Fixture fixture; + fixture.addColumns(1); + CHECK(fixture.layout.setWidthFraction(0, 2.0 / 3)); + + CHECK(fixture.layout.cycleWidth(0, -1)); + CHECK(std::fabs(fixture.layout.widthFraction(0) - 0.5) < 1e-6); + + CHECK(fixture.layout.cycleWidth(0, -1)); + CHECK(std::fabs(fixture.layout.widthFraction(0) - 1.0 / 3) < 1e-6); + + // Wraps back to the last preset. + CHECK(fixture.layout.cycleWidth(0, -1)); + CHECK(std::fabs(fixture.layout.widthFraction(0) - 2.0 / 3) < 1e-6); +} + UMBRIEL_TEST(toggleFullWidthRestoresThePreviousFraction) { Fixture fixture; fixture.addColumns(2);