Add max-rows

Scrolling left the overlay's height entirely derived — as many rows as fit
in 90% of the display — so with enough windows it is always nearly
full-height, and there was no way to ask for a compact strip instead.
max-rows caps the viewport and scrolls the rest, which makes it the
symmetric partner to max-columns; rows are also the natural unit when
tiles are a fixed size, where a height in ppt would flip the row count
about as the tile size changes.

Measured on a 1280x1440 display with 14 tiles at 18ppt: uncapped gives a
981px surface showing 3 of 4 rows, max-rows = 2 gives 657px, and 1 gives
333px — each a row's pitch apart.

The test I wrote for it was wrong before the code was: four tiles make a
2x2 grid under the ceil(sqrt(n)) rule, not the 4x1 I had assumed.
This commit is contained in:
Milad Alizadeh
2026-08-31 18:27:11 +01:00
parent 94d8145079
commit 13c0252cef
4 changed files with 43 additions and 7 deletions
+4 -1
View File
@@ -53,7 +53,9 @@ config:
tile-width = 18ppt # how big a thumbnail is
tile-height = 20ppt # defaults to the display's aspect
max-columns = 4 # rows beyond the screen scroll
max-columns = 4
max-rows = 3 # default: as many as the display fits;
# rows beyond that scroll
font = monospace # also --font
font-size = 13.3
@@ -159,6 +161,7 @@ impl Args {
tile_w: tile_w.max(1),
tile_h: tile_h.max(1),
max_cols: cfg.max_columns.unwrap_or(base.max_cols).max(1),
max_rows: cfg.max_rows.map(|r| r.max(1)).or(base.max_rows),
labels: self.labels.or(cfg.labels).unwrap_or(base.labels),
font: self
.font
+5 -3
View File
@@ -88,6 +88,7 @@ pub struct Config {
pub tile_width: Option<Length>,
pub tile_height: Option<Length>,
pub max_columns: Option<i32>,
pub max_rows: Option<i32>,
pub font: Option<String>,
pub font_size: Option<f32>,
pub labels: Option<bool>,
@@ -140,9 +141,8 @@ impl Config {
"border-width" => self.border_width = Some(Length::parse(value)?),
"tile-width" => self.tile_width = Some(Length::parse(value)?),
"tile-height" => self.tile_height = Some(Length::parse(value)?),
"max-columns" => {
self.max_columns = Some(number(value)?);
}
"max-columns" => self.max_columns = Some(number(value)?),
"max-rows" => self.max_rows = Some(number(value)?),
"font" => self.font = Some(value.to_string()),
"font-size" => self.font_size = Some(number(value)?),
"labels" => self.labels = Some(boolean(value)?),
@@ -230,6 +230,7 @@ border-width = 2px
tile-width = 18ppt
max-columns = 4
max-rows = 3
live = current
fps = 30
@@ -242,6 +243,7 @@ labels = no
assert_eq!(cfg.border_width, Some(Length::Px(2)));
assert_eq!(cfg.tile_width, Some(Length::Ppt(18.0)));
assert_eq!(cfg.max_columns, Some(4));
assert_eq!(cfg.max_rows, Some(3));
assert_eq!(cfg.fps, Some(30));
assert_eq!(cfg.labels, Some(false));
assert!(cfg.live.is_some());
+26 -1
View File
@@ -25,6 +25,9 @@ pub struct Theme {
/// Margin between the grid and the window edge.
pub margin: i32,
pub max_cols: i32,
/// Cap on rows shown at once. Without it the viewport is as tall as the
/// display allows; with it the grid stays compact and scrolls sooner.
pub max_rows: Option<i32>,
/// Gap between a thumbnail and its label (rasi `element { spacing }`).
pub spacing: i32,
/// Label font family, resolved against the system's fonts. The default is
@@ -53,6 +56,7 @@ impl Default for Theme {
gap: 15,
margin: 12,
max_cols: 4,
max_rows: None,
spacing: 10,
font: crate::text::SYSTEM_MONO.to_string(),
font_px: 13.3,
@@ -111,7 +115,8 @@ impl Layout {
let fit_cols = ((room_w + t.gap) / (elem_w + t.gap)).max(1);
cols = cols.clamp(1, t.max_cols.min(fit_cols)).max(1);
let rows = (n + cols - 1) / cols;
let visible_rows = ((room_h + t.gap) / (elem_h + t.gap)).clamp(1, rows.max(1));
let fits = (room_h + t.gap) / (elem_h + t.gap);
let visible_rows = fits.min(t.max_rows.unwrap_or(fits)).clamp(1, rows.max(1));
Self {
cols,
@@ -472,6 +477,26 @@ mod tests {
);
}
#[test]
fn max_rows_keeps_the_grid_compact() {
let mut t = Theme::default();
let full = Layout::new(&t, 30, (1280, 1440));
t.max_rows = Some(2);
let capped = Layout::new(&t, 30, (1280, 1440));
assert!(
capped.visible_rows == 2 && full.visible_rows > 2,
"{capped:?}"
);
assert!(capped.height < full.height, "a shorter overlay");
assert!(capped.scrollable());
// The cap cannot invent rows: four tiles make a 2x2 grid, and a cap of
// five leaves it alone.
t.max_rows = Some(5);
let few = Layout::new(&t, 4, (1280, 1440));
assert_eq!((few.cols, few.rows, few.visible_rows), (2, 2, 2), "{few:?}");
assert!(!few.scrollable());
}
#[test]
fn revealing_moves_the_viewport_as_little_as_possible() {
let t = Theme::default();