crates/veilvoice-gui/src/window.rs
what this file is for · veilvoice-gui · 244 lines · the same file on GitHub
The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.
// SPDX-License-Identifier: GPL-3.0-or-later
//! How big the window opens, and why it is not a constant.
//!
//! # The two failures a single number produces
//!
//! The window used to open at 1100 by 720 always. That is too small for this
//! application: the longest panel is 1288 pixels of content, so it opened on a
//! panel with its bottom third missing, and a reader had to discover the
//! scrollbar to find out there was more.
//!
//! Opening at 1400 by 1000 instead fixes that and breaks something else. A
//! 1366 by 768 laptop is still a common machine, and a window taller than the
//! screen opens with its lower edge, and often its buttons, off the bottom.
//! That looks like a broken program rather than a generous one.
//!
//! So the size is not a constant. It is a preference, clamped to what the
//! screen can actually show, worked out on the first frame when the monitor
//! size is known. On a large display the window opens big enough to read; on a
//! small one it opens as big as fits and scrolls, which is what scrolling is
//! for.
//!
//! # The floor is about width, and it stays where it was
//!
//! Everything is inside one scroll area, so a short window loses nothing.
//! Width is different: the layout is column-based and below roughly 720 across
//! the columns start to overlap. That floor is enforced by the window manager
//! through `with_min_inner_size`, so a person who wants a small window can
//! still drag it down to the floor. Nothing here stops them; it stops the
//! program from *opening* somewhere unreadable, which is a different thing.
/// The size the window opens at when the screen has room for it.
///
/// Measured rather than picked. At 1400 across, the nine tab labels fit with
/// room to spare and the longest panel is 1288 pixels tall; 1000 shows all of
/// every panel but that one, and that one scrolls.
pub const PREFERRED: [f32; 2] = [1400.0, 1000.0];
/// The floor, enforced by the window manager.
pub const MINIMUM: [f32; 2] = [720.0, 520.0];
/// What the window has to leave for the desktop, in pixels.
///
/// A proportion is the wrong model here and was the first thing tried: nine
/// tenths of a 1080-pixel screen is 972, which would shrink the window on the
/// commonest desktop display in the world to avoid a task bar that is 40
/// pixels tall. What the window actually loses is a title bar at the top and
/// a task bar or dock at the bottom, and those are a fixed size whatever the
/// screen is.
///
/// Generous rather than exact, because it cannot be exact: a dock can be
/// enormous, and the cost of over-reserving is a slightly smaller window
/// while the cost of under-reserving is buttons under the task bar.
const CHROME: [f32; 2] = [20.0, 80.0];
/// The size asked for by `--size <W>x<H>`, if one was and it parses.
///
/// A malformed value opens the default window rather than refusing to start.
/// This is a convenience for somebody who wants a bigger window and for the
/// screenshot harness; neither is worth a program that will not open, and a
/// window that is the wrong size says so by being the wrong size.
///
/// Clamped to the minimum the window enforces anyway, so `--size 1x1` gives a
/// usable window instead of one whose columns overlap.
pub fn requested_size() -> Option<[f32; 2]> {
size_from(std::env::args().skip(1))
}
/// The parsing half, separated from the environment so it can be tested.
///
/// `main` cannot be called from a test and `std::env::args` cannot be set by
/// one, so a parser that reads the environment directly is a parser nothing
/// checks. This takes the arguments instead.
pub fn size_from<I: Iterator<Item = String>>(args: I) -> Option<[f32; 2]> {
let mut args = args;
while let Some(arg) = args.next() {
let value = if let Some(rest) = arg.strip_prefix("--size=") {
rest.to_string()
} else if arg == "--size" {
args.next()?
} else {
continue;
};
let (width, height) = value.split_once(['x', 'X'])?;
let width: f32 = width.trim().parse().ok()?;
let height: f32 = height.trim().parse().ok()?;
if !width.is_finite() || !height.is_finite() {
return None;
}
return Some([width.max(720.0), height.max(520.0)]);
}
None
}
/// The size to open at on this screen, given the monitor and what was asked
/// for on the command line.
///
/// `--size` wins outright, clamped only by the floor: somebody who names a
/// size means it, and the screenshot harness needs exactly the size it asked
/// for. Otherwise the preferred size is used, shrunk to fit the screen.
pub fn opening_size(monitor: Option<[f32; 2]>, asked_for: Option<[f32; 2]>) -> [f32; 2] {
if let Some(asked) = asked_for {
return asked;
}
let Some(monitor) = monitor else {
// No monitor size to consult. The preferred size is the better guess:
// opening too small has been the actual complaint, and a screen small
// enough for it to matter will report itself on the next frame.
return PREFERRED;
};
[
PREFERRED[0].min(monitor[0] - CHROME[0]).max(MINIMUM[0]),
PREFERRED[1].min(monitor[1] - CHROME[1]).max(MINIMUM[1]),
]
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Option<[f32; 2]> {
size_from(args.iter().map(|a| a.to_string()))
}
#[test]
fn a_size_is_read_in_either_spelling() {
assert_eq!(parse(&["--size", "1400x1000"]), Some([1400.0, 1000.0]));
assert_eq!(parse(&["--size=1400x1000"]), Some([1400.0, 1000.0]));
assert_eq!(parse(&["--size", "1400X1000"]), Some([1400.0, 1000.0]));
}
#[test]
fn the_size_is_found_beside_other_arguments() {
assert_eq!(
parse(&["--tab", "verify", "--size", "1400x1000"]),
Some([1400.0, 1000.0])
);
}
/// The window enforces a floor of 720x520, so a smaller request is raised
/// to it rather than producing a window whose columns overlap.
#[test]
fn a_size_below_the_minimum_is_raised_to_it() {
assert_eq!(parse(&["--size", "1x1"]), Some([720.0, 520.0]));
assert_eq!(parse(&["--size", "1400x10"]), Some([1400.0, 520.0]));
}
/// A value that makes no sense opens the default window. The alternative
/// is a program that will not start over a convenience, and a window of
/// the wrong size reports itself by being the wrong size.
#[test]
fn nonsense_falls_back_to_the_default() {
for bad in [
"",
"1400",
"widexhigh",
"1400x",
"x1000",
"nanxnan",
"infxinf",
] {
assert_eq!(parse(&["--size", bad]), None, "{bad:?} should not parse");
}
assert_eq!(parse(&["--tab", "file"]), None);
assert_eq!(parse(&[]), None);
}
/// The help text has to name the option, because the manual page is
/// generated from that text: an option the program accepts and the help
/// does not mention is one nobody can find.
#[test]
fn the_help_text_mentions_the_size_option() {
let source = include_str!("main.rs");
let usage = source
.split("const USAGE: &str = \"\\\n")
.nth(1)
.and_then(|rest| rest.split("\";").next())
.expect("the usage text has to be findable");
assert!(
usage.contains("--size"),
"`--help` does not mention --size, which the program accepts"
);
}
}
#[cfg(test)]
mod opening {
use super::*;
/// A big screen gets the size the application actually wants.
#[test]
fn a_large_screen_gets_the_preferred_size() {
assert_eq!(opening_size(Some([2560.0, 1440.0]), None), PREFERRED);
assert_eq!(opening_size(Some([3840.0, 2160.0]), None), PREFERRED);
}
/// The machine this is really about: a window taller than the screen opens
/// with its buttons off the bottom.
#[test]
fn a_small_laptop_gets_a_window_that_fits_on_it() {
let screen = [1366.0, 768.0];
let size = opening_size(Some(screen), None);
assert!(
size[0] <= screen[0] - CHROME[0] && size[1] <= screen[1] - CHROME[1],
"{size:?} does not leave room for a title bar and a task bar on a \
{screen:?} screen"
);
assert!(size[0] >= MINIMUM[0] && size[1] >= MINIMUM[1]);
}
/// 1080p is the commonest desktop screen there is, and the window should
/// open at its full preferred size on one. A proportional allowance got
/// this wrong, which is why the allowance is a number of pixels.
#[test]
fn the_commonest_screen_of_all_gets_the_full_size() {
assert_eq!(opening_size(Some([1920.0, 1080.0]), None), PREFERRED);
}
/// Never below the floor, however small the screen claims to be. A window
/// narrower than this has overlapping columns, which is worse than a
/// window that does not fit.
#[test]
fn a_tiny_screen_still_gets_a_readable_window() {
let size = opening_size(Some([320.0, 200.0]), None);
assert_eq!(size, MINIMUM);
}
/// `--size` is somebody saying what they want, including the screenshot
/// harness, which needs the exact size or the pictures are not comparable.
#[test]
fn an_explicit_size_is_not_second_guessed() {
assert_eq!(
opening_size(Some([1366.0, 768.0]), Some([1400.0, 1900.0])),
[1400.0, 1900.0]
);
}
/// With nothing known about the screen, the preferred size is the answer:
/// opening too small is the complaint, and the first frame will report the
/// monitor if it turns out to matter.
#[test]
fn an_unknown_screen_gets_the_preferred_size() {
assert_eq!(opening_size(None, None), PREFERRED);
}
}