use eframe::egui::{self, Vec2};
const TOOLBAR_FRACTION: f32 = 0.07;
const MIN_BUTTON_PX: f32 = 40.0;
pub const FULL_TOOLBAR_WIDTH: f32 = 600.0;
pub const COMPACT_TOOLBAR_WIDTH: f32 = 290.0;
const MAX_MONITOR_FRACTION: f32 = 0.9;
pub const DEFAULT_WINDOW: Vec2 = Vec2::new(1024.0, 720.0);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UiScale {
Auto,
Fixed(f32),
}
impl UiScale {
pub fn parse(text: &str) -> Option<Self> {
let text = text.trim();
if text.eq_ignore_ascii_case("auto") {
return Some(Self::Auto);
}
text.parse::<f32>()
.ok()
.filter(|f| f.is_finite() && *f > 0.0)
.map(|f| Self::Fixed(f.clamp(0.25, 4.0)))
}
pub fn from_env() -> Self {
std::env::var("BOBVIEW_UI_SCALE")
.ok()
.and_then(|v| {
let scale = Self::parse(&v);
if scale.is_none() {
log::warn!("ignoring BOBVIEW_UI_SCALE={v}: expected a number or auto");
}
scale
})
.unwrap_or(Self::Auto)
}
pub fn zoom_factor(self, native: f32, screen_px: f32, toolbar: f32, button: f32) -> f32 {
match self {
Self::Fixed(f) => f,
Self::Auto => auto_pixels_per_point(native, screen_px, toolbar, button) / native,
}
}
}
fn auto_pixels_per_point(native: f32, screen_px: f32, toolbar: f32, button: f32) -> f32 {
let native = native.max(0.1);
let cap = TOOLBAR_FRACTION * screen_px / toolbar.max(1.0);
let floor = (MIN_BUTTON_PX / button.max(1.0)).max(1.0).min(native);
cap.min(native).max(floor)
}
pub fn changed(old: f32, new: f32) -> bool {
(old - new).abs() > 0.01 * old.abs().max(1e-3)
}
pub fn window_size(display: Vec2, chrome: f32, monitor: Option<Vec2>) -> Vec2 {
let mut display = display;
if let Some(m) = monitor.filter(|m| m.x > 0.0 && m.y > 0.0) {
let room = m * MAX_MONITOR_FRACTION - Vec2::new(1.0, chrome.ceil() + 1.0);
let fit = (room.x / display.x).min(room.y / display.y);
if fit < 1.0 && fit > 0.0 {
display *= fit;
}
}
Vec2::new(
display.x.ceil().max(COMPACT_TOOLBAR_WIDTH) + 1.0,
display.y.ceil() + chrome.ceil() + 1.0,
)
}
pub fn can_resize(info: &egui::ViewportInfo, zoom_factor: f32) -> bool {
info.fullscreen != Some(true)
&& info.maximized != Some(true)
&& !small_screen(info, zoom_factor)
}
pub fn small_screen(info: &egui::ViewportInfo, zoom_factor: f32) -> bool {
info.monitor_size
.map(|m| m * zoom_factor)
.is_some_and(|m| m.x < DEFAULT_WINDOW.x || m.y < DEFAULT_WINDOW.y)
}
pub struct Chrome {
pub scale: UiScale,
pub toolbar: f32,
pub button: f32,
pub panels: f32,
settled: bool,
resize: bool,
pub auto_size: bool,
first: bool,
hold: bool,
fit: bool,
}
impl Chrome {
pub fn new(scale: UiScale) -> Self {
Self {
scale,
toolbar: 32.0,
button: 26.0,
panels: 56.0,
settled: false,
resize: false,
auto_size: true,
first: true,
hold: false,
fit: false,
}
}
pub fn settled(&self) -> bool {
self.settled
}
pub fn hold_size(&mut self) {
self.first = false;
self.hold = true;
}
pub fn fit_window(&mut self) {
self.resize = true;
self.hold = false;
self.fit = true;
}
pub fn display_changed(&mut self) {
self.resize = true;
}
pub fn begin(&mut self, ctx: &egui::Context) {
let native = ctx.native_pixels_per_point().unwrap_or(1.0);
let ppp = ctx.pixels_per_point();
let (window, monitor) = ctx.input(|i| (i.content_rect().size(), i.viewport().monitor_size));
let screen_px = monitor.filter(|m| m.y > 0.0).unwrap_or(window).y * ppp;
let zoom = self
.scale
.zoom_factor(native, screen_px, self.toolbar, self.button);
self.settled = !changed(ctx.zoom_factor(), zoom);
if !self.settled {
ctx.set_zoom_factor(zoom);
}
}
pub fn end(&mut self, ctx: &egui::Context, display: Option<Vec2>) {
if !self.resize || !self.settled {
return;
}
let Some(display) = display else {
self.resize = false;
self.hold = false;
self.fit = false;
return;
};
let (info, inner) = ctx.input(|i| (i.viewport().clone(), i.content_rect().size()));
if info.monitor_size.is_none() {
return;
}
self.resize = false;
if std::mem::take(&mut self.hold) {
return;
}
let fit = std::mem::take(&mut self.fit);
let follow = self.first || self.auto_size || fit;
self.first = false;
if !follow || !can_resize(&info, ctx.zoom_factor()) {
return;
}
let size = window_size(display, self.panels, info.monitor_size);
if (size - inner).length() >= 1.0 {
log::debug!("size the window from {inner:?} to {size:?} for the display");
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_scales() {
assert_eq!(UiScale::parse("auto"), Some(UiScale::Auto));
assert_eq!(UiScale::parse(" 0.5 "), Some(UiScale::Fixed(0.5)));
assert_eq!(UiScale::parse("100"), Some(UiScale::Fixed(4.0)));
assert_eq!(UiScale::parse("0"), None);
assert_eq!(UiScale::parse("-1"), None);
assert_eq!(UiScale::parse("big"), None);
}
#[test]
fn desktop_keeps_its_scale() {
assert_eq!(auto_pixels_per_point(1.0, 1080.0, 32.0, 26.0), 1.0);
assert_eq!(auto_pixels_per_point(2.0, 2160.0, 32.0, 26.0), 2.0);
assert_eq!(auto_pixels_per_point(1.0, 300.0, 32.0, 26.0), 1.0);
}
#[test]
fn phone_shrinks_to_a_share_of_the_height() {
assert_eq!(auto_pixels_per_point(2.75, 2340.0, 32.0, 26.0), 2.75);
let p = auto_pixels_per_point(2.75, 1080.0, 32.0, 26.0);
assert!((p * 32.0 - 75.6).abs() < 0.1, "{p}");
let p = auto_pixels_per_point(2.75, 540.0, 32.0, 26.0);
assert!((p * 26.0 - 40.0).abs() < 0.1, "{p}");
}
#[test]
fn fixed_scale_wins() {
assert_eq!(
UiScale::Fixed(0.5).zoom_factor(3.0, 2000.0, 32.0, 26.0),
0.5
);
let auto = UiScale::Auto.zoom_factor(2.75, 1080.0, 32.0, 26.0);
assert!(auto < 1.0 && auto > 0.5, "{auto}");
}
#[test]
fn windows_fit_the_display_and_the_monitor() {
let s = window_size(
Vec2::new(438.0, 80.0),
60.0,
Some(Vec2::new(1920.0, 1080.0)),
);
assert_eq!(s, Vec2::new(439.0, 141.0));
let s = window_size(Vec2::new(100.0, 80.0), 60.0, None);
assert_eq!(s.x, COMPACT_TOOLBAR_WIDTH + 1.0);
let monitor = Some(Vec2::new(1920.0, 1080.0));
let s = window_size(Vec2::new(3000.0, 2000.0), 60.0, monitor);
assert!(s.y <= 973.0 && (s.x - 1368.0).abs() <= 1.0, "{s:?}");
let s = window_size(Vec2::new(3000.0, 500.0), 60.0, monitor);
assert!((s.x - 1728.0).abs() <= 1.0, "{s:?}");
assert!(
(s.y - (500.0 * 1727.0 / 3000.0 + 61.0)).abs() <= 1.5,
"{s:?}"
);
}
#[test]
fn resizes_only_free_windows_on_big_screens() {
let mut info = egui::ViewportInfo {
monitor_size: Some(Vec2::new(1920.0, 1080.0)),
..Default::default()
};
assert!(can_resize(&info, 1.0));
info.maximized = Some(true);
assert!(!can_resize(&info, 1.0));
info.maximized = Some(false);
info.fullscreen = Some(true);
assert!(!can_resize(&info, 1.0));
info.fullscreen = None;
info.monitor_size = Some(Vec2::new(392.0, 851.0));
assert!(!can_resize(&info, 1.0));
assert!(!can_resize(&info, 0.8));
}
}