diff --git a/README.md b/README.md index 380d54b..8c47d7d 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ outputs = yes live = all fps = 12 format = tsv +timeout = 0 # seconds; 0 means none ``` Sizes take sway's units: `600px` is absolute, `90ppt` a percentage — and the diff --git a/src/app.rs b/src/app.rs index 92d2ae3..f3e650d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -56,12 +56,9 @@ pub struct Settings { pub theme: Theme, pub live: Live, pub fps: u32, - /// Integer scale of the display the overlay renders on. + /// Integer scale of the display the overlay renders on, and its name, so + /// the overlay maps there rather than wherever the compositor would put it. pub scale: i32, - /// That display's logical size, which the grid is fitted into. - pub display: (i32, i32), - /// And its name, so the overlay maps there rather than wherever the - /// compositor would have put it. pub output: String, } @@ -90,8 +87,6 @@ pub struct App { pub(crate) sel: usize, /// First row of the grid on screen. The rest scroll. pub(crate) scroll: i32, - /// Set when the viewport moved and the subsurfaces need re-placing. - pub(crate) needs_tiles: bool, pub(crate) shift: bool, /// Where the pointer is, and which tile it pressed. Hovering deliberately @@ -158,16 +153,15 @@ impl App { qh: &QueueHandle, targets: Vec, settings: Settings, + layout: Layout, ) -> Result> { let Settings { theme, live, fps, scale, - display, output, } = settings; - let layout = Layout::new(&theme, targets.len() as i32, display); // Bind everything up front so a compositor missing a protocol fails // here, with a name, rather than halfway through a capture. let mut app = Self { @@ -190,7 +184,6 @@ impl App { scale, sel: 0, scroll: 0, - needs_tiles: false, shift: false, hover: None, pressed: None, diff --git a/src/cli.rs b/src/cli.rs index 83cf341..e210502 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -64,6 +64,7 @@ config: live = all fps = 12 format = tsv + timeout = 0 # seconds; 0 means none formats: @@ -118,6 +119,8 @@ pub struct Options { pub format: Format, pub outputs: bool, pub timeout: Option, + /// The logical size of the display the grid will be laid out for. + pub display: (i32, i32), pub settings: Settings, } @@ -182,12 +185,12 @@ impl Args { format: self.format.or(cfg.format).unwrap_or(Format::Tsv), outputs: self.outputs.or(cfg.outputs).unwrap_or(true), timeout: self.timeout.or(cfg.timeout), + display: (display.width, display.height), settings: Settings { theme, live: self.live.or(cfg.live).unwrap_or(Live::All), fps: self.fps.or(cfg.fps).unwrap_or(12), scale: display.scale, - display: (display.width, display.height), output: display.name.clone(), }, } @@ -226,7 +229,7 @@ pub fn parse_args() -> Result { "--timeout" => { let v = it.next().ok_or("--timeout needs seconds")?; let secs: f64 = v.parse().map_err(|_| format!("bad --timeout: {v}"))?; - args.timeout = Some(Duration::from_secs_f64(secs)); + args.timeout = (secs > 0.0).then(|| Duration::from_secs_f64(secs)); } "-h" | "--help" => { print!("{HELP}"); diff --git a/src/config.rs b/src/config.rs index 8210437..48ea06e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -151,7 +151,12 @@ impl Config { "live" => self.live = Some(Live::parse(value)?), "fps" => self.fps = Some(number(value)?), "format" => self.format = Some(Format::parse(value)?), - "timeout" => self.timeout = Some(Duration::from_secs_f64(number(value)?)), + // Zero is how you say "no timeout"; an immediate deadline would + // only ever be a mistake. + "timeout" => { + let secs: f64 = number(value)?; + self.timeout = (secs > 0.0).then(|| Duration::from_secs_f64(secs)); + } other => return Err(format!("unknown setting {other:?}")), } Ok(()) @@ -236,6 +241,7 @@ max-rows = 3 live = current fps = 30 labels = no +timeout = 0 ", ) .expect("should parse"); @@ -247,6 +253,7 @@ labels = no assert_eq!(cfg.max_rows, Some(3)); assert_eq!(cfg.fps, Some(30)); assert_eq!(cfg.labels, Some(false)); + assert_eq!(cfg.timeout, None, "zero means no timeout"); assert!(cfg.live.is_some()); // Untouched settings stay unset, so defaults survive. assert_eq!(cfg.foreground, None); diff --git a/src/main.rs b/src/main.rs index ea15c44..cf7d727 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,9 +57,8 @@ fn main() -> ExitCode { } fn run() -> Result> { - let args = cli::parse_args().map_err(|e| -> Box { e.into() })?; - let config = - Config::load(args.config.as_deref()).map_err(|e| -> Box { e.into() })?; + let args = cli::parse_args().map_err(Box::::from)?; + let config = Config::load(args.config.as_deref()).map_err(Box::::from)?; let start = Instant::now(); let mut phases = Phases::new(args.verbose); @@ -88,29 +87,29 @@ fn run() -> Result> { phases.mark("sway-tree"); cli::arm_timeout(opts.timeout); - let settings = opts.settings; + let (display, settings) = (opts.display, opts.settings); let theme = &settings.theme; let scale = settings.scale; + // The grid is measured once here: the label shaping below and the overlay + // itself must agree about how wide a label may be. + let layout = Layout::new(theme, targets.len() as i32, display); // Start shaping labels now: it costs ~55ms of font loading and glyph // rasterising, and the captures below are ~55ms of waiting on the // compositor, so the two overlap almost exactly. - let labels = theme.labels.then(|| { - let layout = Layout::new(theme, targets.len() as i32, settings.display); + let labels = layout.label(0, 0).map(|label| { text::spawn( targets.iter().map(Target::label).collect(), theme.font.clone(), theme.font_px * scale as f32, (theme.line_h * scale) as f32, - // The label box is a tile wide; with no tiles there is nothing to - // shape anyway. - (layout.label(0, 0).map(|r| r.w).unwrap_or(1) * scale) as f32, + (label.w * scale) as f32, ) }); let conn = Connection::connect_to_env()?; let (globals, mut queue) = registry_queue_init::(&conn)?; let qh = queue.handle(); - let mut app = App::new(&globals, &qh, targets, settings)?; + let mut app = App::new(&globals, &qh, targets, settings, layout)?; // Two roundtrips: one for the toplevel list, one for each handle's state. queue.roundtrip(&mut app)?; @@ -141,15 +140,7 @@ fn run() -> Result> { conn.flush()?; phases.mark("mapped"); - // Scrolling re-places the subsurfaces; doing it here rather than inside the - // key handler coalesces a held-down arrow into one update per dispatch. - while !app.finished() { - queue.blocking_dispatch(&mut app)?; - if std::mem::take(&mut app.needs_tiles) { - app.sync_tiles(&qh); - conn.flush()?; - } - } + pump(&mut queue, &mut app, |a| a.finished())?; if opts.verbose { app.report(start.elapsed()); } diff --git a/src/overlay.rs b/src/overlay.rs index bed0a5a..29b0703 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -236,30 +236,31 @@ impl App { surface.commit(); } - fn move_sel(&mut self, delta: i32) { + fn move_sel(&mut self, delta: i32, qh: &QueueHandle) { let n = self.tiles.len() as i32; if n == 0 { return; } - self.select((self.sel as i32 + delta).rem_euclid(n) as usize); + self.select((self.sel as i32 + delta).rem_euclid(n) as usize, qh); } - fn move_row(&mut self, rows: i32) { + fn move_row(&mut self, rows: i32, qh: &QueueHandle) { let n = self.tiles.len() as i32; let target = self.sel as i32 + rows * self.layout.cols; if target >= 0 && target < n { - self.select(target as usize); + self.select(target as usize, qh); } } /// Move the selection, scrolling the least that keeps it on screen. Every - /// keyboard move goes through here, so the selection is never off-view. - fn select(&mut self, i: usize) { + /// move goes through here, so the selection is never off-view and the + /// subsurfaces always match the viewport. + fn select(&mut self, i: usize, qh: &QueueHandle) { self.sel = i; let scroll = self.layout.reveal(i, self.scroll); if scroll != self.scroll { self.scroll = scroll; - self.needs_tiles = true; + self.sync_tiles(qh); } self.paint(); } @@ -294,7 +295,7 @@ impl App { } } - fn key(&mut self, code: u32) { + fn key(&mut self, code: u32, qh: &QueueHandle) { match code { KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true, KEY_ESC | KEY_Q => self.ending = Ending::Cancelled, @@ -302,15 +303,15 @@ impl App { self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); self.ending = Ending::Picked; } - KEY_TAB if self.shift => self.move_sel(-1), - KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1), - KEY_LEFT | KEY_H => self.move_sel(-1), - KEY_DOWN | KEY_J => self.move_row(1), - KEY_UP | KEY_K => self.move_row(-1), - KEY_HOME => self.select(0), - KEY_END => self.select(self.tiles.len().saturating_sub(1)), - KEY_PGUP => self.move_row(-self.layout.visible_rows), - KEY_PGDN => self.move_row(self.layout.visible_rows), + KEY_TAB if self.shift => self.move_sel(-1, qh), + KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh), + KEY_LEFT | KEY_H => self.move_sel(-1, qh), + KEY_DOWN | KEY_J => self.move_row(1, qh), + KEY_UP | KEY_K => self.move_row(-1, qh), + KEY_HOME => self.select(0, qh), + KEY_END => self.select(self.tiles.len().saturating_sub(1), qh), + KEY_PGUP => self.move_row(-self.layout.visible_rows, qh), + KEY_PGDN => self.move_row(self.layout.visible_rows, qh), _ => {} } } @@ -373,11 +374,11 @@ impl Dispatch for App { event: wl_keyboard::Event, _: &(), _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, ) { if let wl_keyboard::Event::Key { key, state, .. } = event { match state { - WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key), + WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key, qh), WEnum::Value(wl_keyboard::KeyState::Released) if key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT => { @@ -399,7 +400,7 @@ impl Dispatch for App { event: wl_pointer::Event, _: &(), _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, ) { match event { wl_pointer::Event::Enter { @@ -437,7 +438,9 @@ impl Dispatch for App { state: WEnum::Value(state), .. } => app.click(state == wl_pointer::ButtonState::Pressed), - wl_pointer::Event::Axis { value, .. } => app.move_sel(if value > 0.0 { 1 } else { -1 }), + wl_pointer::Event::Axis { value, .. } => { + app.move_sel(if value > 0.0 { 1 } else { -1 }, qh) + } _ => {} } } diff --git a/src/theme.rs b/src/theme.rs index 3440841..5255b21 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,6 +1,14 @@ -//! Look and layout, ported from the rofi setup this replaces (mytheme.rasi + -//! the -theme-str rofigrid builds): gruvbox dark, a yellow selection that fills -//! the element padding, and a window that hugs the grid. +//! Look and layout. +//! +//! The colours and spacing come from the rofi setup this replaces (mytheme.rasi +//! plus the -theme-str rofigrid built): gruvbox dark, a yellow selection filling +//! the element padding, `title · app` centred under each thumbnail. +//! +//! Sizing works from caps rather than from a thumbnail size. The config gives a +//! box the grid may fill and a column and row limit; a thumbnail is that box +//! divided by those limits. So a thumbnail is the same size whether one window +//! is open or thirty — the overlay hugs whatever is there, and rows past the +//! limit scroll. /// 0xAARRGGBB, premultiplied (everything here is opaque). pub type Argb = u32; @@ -51,10 +59,10 @@ impl Default for Theme { sel_fg: 0xff282828, border: 0xffd79921, border_px: 2, - // Placeholders: the command line resolves these against the - // display the grid will appear on. - max_w: 1152, - max_h: 1296, + // No cap of their own: Layout clamps to the display, and the + // command line resolves the configured percentage over the top. + max_w: i32::MAX, + max_h: i32::MAX, pad: 12, gap: 15, margin: 12, @@ -75,13 +83,13 @@ impl Default for Theme { #[derive(Debug)] pub struct Layout { pub cols: i32, - /// Rows the whole grid needs, and how many of them fit on screen at once. + /// Rows the whole grid needs, and how many of them are on screen at once. pub rows: i32, pub visible_rows: i32, - /// How many tiles there are, which the last row may not fill. - n: i32, pub width: i32, pub height: i32, + /// How many tiles there are, which the last row may not fill. + n: i32, elem_w: i32, elem_h: i32, margin: i32, @@ -94,13 +102,14 @@ pub struct Layout { } impl Layout { - /// A balanced grid: ceil(sqrt(n)) columns, capped, so the last row isn't - /// ragged (6 windows -> 3x2, not 4x2 with two holes). Same rule rofigrid uses. + /// Lay out `n` tiles for a display of the given logical size. /// - /// A thumbnail is the size that divides the configured box by the column and - /// row caps, so it does not change with how many windows are open: one - /// window gets a normal thumbnail in a small overlay, thirty get the same - /// thumbnail and scroll. The overlay then hugs whatever is actually there. + /// A thumbnail is the configured box divided by the column and row caps, so + /// it does not change with how many windows are open: one window gets a + /// normal thumbnail in a small overlay, thirty get the same thumbnail and + /// scroll. Columns follow ceil(sqrt(n)) up to the cap, so a handful of + /// windows makes a tidy grid rather than one long row — the rule rofigrid + /// used — and the overlay hugs whatever is there. pub fn new(t: &Theme, n: i32, display: (i32, i32)) -> Self { let n = n.max(0); let (cap_cols, cap_rows) = (t.max_cols.max(1), t.max_rows.max(1)); @@ -124,6 +133,7 @@ impl Layout { cols += 1; } cols = cols.clamp(1, cap_cols); + // i32::div_ceil is still unstable; only the unsigned one is not. let rows = (n + cols - 1) / cols; let visible_rows = cap_rows.clamp(1, rows.max(1));