Tidy up after the sizing rework
The caps model landed in pieces, and the pieces left seams. This joins them up, and fixes two things the reread turned up. Deferred subsurface syncing is gone. `needs_tiles` existed because `select` had no queue handle to sync with, so main.rs grew a bespoke event loop to notice the flag afterwards. The dispatch handlers are handed a handle already: pass it down and let `select` do the work itself. main.rs is back to one uniform `pump`. The layout is built once, in run(), and passed to App::new, rather than built there and again inside it. `display` moves from Settings, which is what App needs, to Options, which is what the caller needs it for. `timeout = 0` meant an immediate deadline, so uncommenting the line in the shipped config would have made wl-pick exit before you saw it. Zero now means no timeout, which is what the comment beside it always claimed. `timeout` was also settable but documented nowhere -- not in --help, not in the README. Both now list every key config.rs accepts. Theme::default's max-width/max-height were placeholder pixel counts that happened to match one monitor. They are i32::MAX now: no cap of their own, with Layout clamping to the display.
This commit is contained in:
@@ -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
|
||||
|
||||
+3
-10
@@ -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<Self>,
|
||||
targets: Vec<Target>,
|
||||
settings: Settings,
|
||||
layout: Layout,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
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,
|
||||
|
||||
+5
-2
@@ -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<Duration>,
|
||||
/// 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<Args, String> {
|
||||
"--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}");
|
||||
|
||||
+8
-1
@@ -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);
|
||||
|
||||
+10
-19
@@ -57,9 +57,8 @@ fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
let args = cli::parse_args().map_err(|e| -> Box<dyn Error> { e.into() })?;
|
||||
let config =
|
||||
Config::load(args.config.as_deref()).map_err(|e| -> Box<dyn Error> { e.into() })?;
|
||||
let args = cli::parse_args().map_err(Box::<dyn Error>::from)?;
|
||||
let config = Config::load(args.config.as_deref()).map_err(Box::<dyn Error>::from)?;
|
||||
|
||||
let start = Instant::now();
|
||||
let mut phases = Phases::new(args.verbose);
|
||||
@@ -88,29 +87,29 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
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::<App>(&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<ExitCode, Box<dyn Error>> {
|
||||
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());
|
||||
}
|
||||
|
||||
+24
-21
@@ -236,30 +236,31 @@ impl App {
|
||||
surface.commit();
|
||||
}
|
||||
|
||||
fn move_sel(&mut self, delta: i32) {
|
||||
fn move_sel(&mut self, delta: i32, qh: &QueueHandle<Self>) {
|
||||
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<Self>) {
|
||||
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>) {
|
||||
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<Self>) {
|
||||
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<WlKeyboard, ()> for App {
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
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<WlPointer, ()> for App {
|
||||
event: wl_pointer::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_pointer::Event::Enter {
|
||||
@@ -437,7 +438,9 @@ impl Dispatch<WlPointer, ()> 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)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-16
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user