Skip to content
Closed
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
12 changes: 7 additions & 5 deletions docs/user/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ expand_single_column = true
| `default_width_fraction` | float | unset | Initial strip-axis extent for new columns (0.1-1.0). The packaged config sets `0.5`; a matching output or workspace rule can override it. When it is unset at every level, the client chooses its initial extent. |
| `center_underfull_strip` | bool | `true` | Center the complete strip when it is shorter than the viewport. Disable to align it at the start edge. |
| `center_focused` | bool | `false` | Always center the focused column. |
| `expand_single_column` | bool | `false` | Fill the viewport for a workspace's lone tiled column. Client size hints and viewport bounds still apply. The packaged config enables this. |
| `expand_single_column` | bool | `false` | Fill the viewport for a workspace's lone tiled column until its width is explicitly changed. Client size hints and viewport bounds still apply. The packaged config enables this. |

### Horizontal and vertical scrolling

Expand Down Expand Up @@ -154,10 +154,12 @@ does not resize existing columns, and a column moved to another output retains
its stored fraction. Re-tiling a floating window or expelling a window into a
new column creates a column using the current default.

`expand_single_column` affects only how a lone tiled column is displayed. It
does not rewrite the stored fraction, so the configured or client-selected
width applies again when a second column appears. Explicit
`default_maximize` and `default_maximize_to_edges` window rules take precedence.
`expand_single_column` initially fills the viewport without rewriting the
column's stored fraction. Explicitly resizing the column, cycling its width, or
setting its width disables automatic expansion for that column. Otherwise, the
configured or client-selected width applies again when a second column appears.
Explicit `default_maximize` and `default_maximize_to_edges` window rules take
precedence.

When focus moves to a hidden or partially hidden column, Umbriel reveals it by
the shortest distance needed to show it completely. A column entering from the
Expand Down
2 changes: 1 addition & 1 deletion docs/user/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Strut edges are resolved independently. A rule that sets only
| `layout.scrolling.center_underfull_strip` | bool | Center the complete strip whenever it is narrower than the viewport. Disable to left-align underfull strips. |
| `layout.scrolling.center_focused` | bool | Always center the focused column, including when the setting changes on config reload. |
| `layout.scrolling.direction` | string | `"horizontal"` or `"vertical"` scroll axis. |
| `layout.scrolling.expand_single_column` | bool | Fill the viewport for a workspace's lone tiled column, subject to client size hints and viewport bounds. Disable to keep the configured/default width. |
| `layout.scrolling.expand_single_column` | bool | Fill the viewport for a workspace's lone tiled column until its width is explicitly changed. Client size hints and viewport bounds still apply. Disable to always keep the configured/default width. |
| `layout.master.position` | string | Side occupied by the master area: `"left"` or `"right"`. |
| `layout.master.default_width_fraction` | float | Master area fraction when both areas exist (0.1-0.9). |
| `layout.master.new_on_top` | bool | Place newly opened windows at the top of the stack. Disable to place them at the bottom. |
Expand Down
2 changes: 1 addition & 1 deletion examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ bottom = 0
default_width_fraction = 0.5 # Initial width for new columns, 0.1-1.0
center_underfull_strip = true # Center a strip narrower than the viewport
center_focused = false # Always center the focused column
expand_single_column = true # Let a lone column fill the viewport
expand_single_column = true # Fill a lone column until its width is explicitly changed

[layout.dwindle]
# preserve_split = false # Keep every split direction fixed after creation
Expand Down
1 change: 1 addition & 0 deletions src/layout/layout.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ namespace umbriel {
double bottomGapWeight = 0.0;
double widthFrac = 0.5;
double savedWidthFrac = 0.0;
bool autoExpand = true;
};

struct LayoutTarget {
Expand Down
17 changes: 15 additions & 2 deletions src/layout/scrolling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace umbriel {
double bottomGapWeight = 0.0;
double widthFraction = 0.5;
double savedWidthFraction = 0.0;
bool autoExpand = true;
double viewportCenterFraction = 0.5;
};

Expand Down Expand Up @@ -167,6 +168,7 @@ namespace umbriel {
.bottomGapWeight = column.bottomGapWeight,
.widthFraction = column.widthFrac,
.savedWidthFraction = column.savedWidthFrac,
.autoExpand = column.autoExpand,
.viewportCenterFraction = 0.5,
};
if (viewportPrimary > 0) {
Expand Down Expand Up @@ -222,6 +224,7 @@ namespace umbriel {
.bottomGapWeight = saved.bottomGapWeight,
.widthFrac = saved.widthFraction,
.savedWidthFrac = saved.savedWidthFraction,
.autoExpand = saved.autoExpand,
};
for (const ScrollingSnapshot::Row& row : saved.rows) {
View* view = (*resolved)[static_cast<size_t>(row.member)];
Expand Down Expand Up @@ -309,7 +312,7 @@ namespace umbriel {
return std::max(1, viewportPrimary + 2 * edgePad);
}
int width = 0;
if (m_columns.size() == 1 && expandSingleColumn()) {
if (m_columns.size() == 1 && expandSingleColumn() && column.autoExpand) {
// Fill the viewport without touching the stored fraction. Client size hints still apply to tiled columns.
width = viewportPrimary;
} else {
Expand Down Expand Up @@ -346,6 +349,14 @@ namespace umbriel {
return true;
}

bool ScrollingLayout::setUserWidthFraction(int columnIndex, double fraction) {
if (!setWidthFraction(columnIndex, fraction)) {
return false;
}
m_columns[static_cast<size_t>(columnIndex)].autoExpand = false;
return true;
}

int ScrollingLayout::centeringOffset(int viewportPrimary) const {
if (!m_config->scrolling.centerUnderfullStrip) {
return 0;
Expand Down Expand Up @@ -792,6 +803,7 @@ namespace umbriel {
Column& column = m_columns[static_cast<size_t>(columnIndex)];
column.widthFrac = nextFractionPreset(m_config->widthPresets, column.widthFrac, direction);
column.savedWidthFrac = 0.0;
column.autoExpand = false;
return true;
}

Expand All @@ -800,6 +812,7 @@ namespace umbriel {
return false;
}
Column& column = m_columns[static_cast<size_t>(columnIndex)];
column.autoExpand = false;
if (column.savedWidthFrac > 0.0) {
column.widthFrac = column.savedWidthFrac;
column.savedWidthFrac = 0.0;
Expand Down Expand Up @@ -1022,7 +1035,7 @@ namespace umbriel {
const int gap = layout.layoutConfig()->totalGap;
auto setColumnPrimaryPx = [&](int columnIndex, int extent) {
const double fraction = static_cast<double>(extent + gap) / static_cast<double>(viewportPrimary + gap);
layout.setWidthFraction(columnIndex, fraction);
layout.setUserWidthFraction(columnIndex, fraction);
};
const bool centerUnderfullStrip =
m_startStripPrimaryPx < viewportPrimary && layout.layoutConfig()->scrolling.centerUnderfullStrip;
Expand Down
1 change: 1 addition & 0 deletions src/layout/scrolling.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace umbriel {
[[nodiscard]] int columnX(int columnIndex, int viewportPrimary) const;
[[nodiscard]] int columnWidth(int columnIndex, int viewportPrimary) const;
bool setWidthFromPixels(int columnIndex, int viewportPrimary, int width);
bool setUserWidthFraction(int columnIndex, double fraction);
[[nodiscard]] bool isFullWidth(int columnIndex) const override;
[[nodiscard]] int maxScroll(int viewportPrimary) const {
return std::max(0, totalWidth(viewportPrimary) - viewportPrimary);
Expand Down
4 changes: 3 additions & 1 deletion src/workspace/workspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,9 @@ namespace umbriel {
m_focusedView->setMaximizedToEdges(false);
}
const int column = m_layout->columnOf(m_focusedView);
if (!m_layout->setWidthFraction(column, fraction)) {
const bool changed = scrollingLayout() != nullptr ? scrollingLayout()->setUserWidthFraction(column, fraction)
: m_layout->setWidthFraction(column, fraction);
if (!changed) {
return false;
}
wlr_xdg_toplevel_set_maximized(m_focusedView->toplevel(), false);
Expand Down
45 changes: 45 additions & 0 deletions tests/harness/checks/517_expand_single_resize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail

readonly BTN_RIGHT=273
readonly OUTPUT_W=1280
readonly OUTPUT_H=720
readonly POINTER="${UMBRIEL_POINTER_CLIENT:-./build-debug/tests/pointer-client}"

cat >> "$UMBRIEL_CONFIG" <<'EOF'
[animation]
enabled = false
[layout.scrolling]
expand_single_column = true
center_underfull_strip = true
EOF
"$UMBRIEL" msg config-reload > /dev/null

foot --title=expand-single-resize sh -c 'sleep 120' > /dev/null 2>&1 &
for _ in $(seq 60); do
window=$("$UMBRIEL" windows --json | jq -c '.[] | select(.title == "expand-single-resize")')
[[ -n $window ]] && break
sleep 0.1
done
if [[ -z ${window:-} ]]; then
echo "timed out waiting for expand-single-resize"
exit 1
fi

before=$window
"$POINTER" "$OUTPUT_W" "$OUTPUT_H" \
move "$(jq -r '.x + .w - 20 | floor' <<< "$before")" "$(jq -r '.y + .h / 2 | floor' <<< "$before")" \
mod logo press "$BTN_RIGHT" move 1000 360 release "$BTN_RIGHT" mod none

for _ in $(seq 60); do
after=$("$UMBRIEL" windows --json | jq -c '.[] | select(.title == "expand-single-resize")')
[[ $(jq -r '.w' <<< "$after") -lt $(jq -r '.w' <<< "$before") ]] && break
sleep 0.1
done

if [[ $(jq -r '.w' <<< "$after") -ge $(jq -r '.w' <<< "$before") ]]; then
echo "right-edge drag did not shrink the expanded lone column: before=$before after=$after"
exit 1
fi

echo "right-edge drag resized an automatically expanded lone column: before=$(jq -r '.w' <<< "$before") after=$(jq -r '.w' <<< "$after")"
22 changes: 22 additions & 0 deletions tests/unit/scrolling_layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ UMBRIEL_TEST(expandSingleColumnTrueFillsALoneColumn) {
CHECK_EQ(fixture.layout.columnWidth(0, kViewport), kViewport);
}

UMBRIEL_TEST(expandSingleColumnSurvivesInitialWidthSeeding) {
Fixture fixture;
fixture.config.scrolling.expandSingleColumn = true;
fixture.addColumns(1);
CHECK(fixture.layout.setWidthFraction(0, 0.5));
CHECK_EQ(fixture.layout.columnWidth(0, kViewport), kViewport);
}

UMBRIEL_TEST(expandSingleColumnTrueHonorsClientMaxWidth) {
Fixture fixture;
fixture.config.scrolling.expandSingleColumn = true;
Expand All @@ -339,6 +347,20 @@ UMBRIEL_TEST(expandSingleColumnTrueReexpandsTheLastSurvivor) {
CHECK_EQ(fixture.layout.columnWidth(0, kViewport), kViewport);
}

UMBRIEL_TEST(pointerResizeOverridesSingleColumnExpansion) {
Fixture fixture;
fixture.config.scrolling.expandSingleColumn = true;
fixture.addColumns(1);
fixture.layout.arrange(kUsable);

auto resize = fixture.layout.beginResize(stub(0), WLR_EDGE_RIGHT, kUsable);
CHECK(resize != nullptr);
resize->applyDelta(-100.0, 0.0, kUsable);
fixture.layout.arrange(kUsable);

CHECK_EQ(fixture.layout.columnWidth(0, kViewport), kViewport - 100);
}

UMBRIEL_TEST(twoHalfColumnsTileExactlyAcrossTheViewport) {
Fixture fixture;
fixture.addColumns(2);
Expand Down