//! How large the reader shows mail: a scale of its messages, as a browser //! zooms a page, with the text laid out again to the reader's width. //! Ctrl+= and Ctrl+- step it, Ctrl+0 puts it back, and a pinch or //! Ctrl+wheel sets any scale between. Every account's reader shares one, //! saved in the top state directory. const std = @import("std"); const Settings = @import("Settings.zig"); const math = std.math; const mem = std.mem; const testing = std.testing; const Zoom = @This(); scale: f32, settings: ?Settings, pub const min = 0.5; pub const max = 3; /// Where Ctrl+= and Ctrl+- stop, as browsers do. const steps = [_]f32{ 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3 }; pub const file = "zoom"; /// The scale saved in `settings`, or 1 when none was or it is not one. pub fn load(settings: ?Settings) Zoom { var buf: [16]u8 = undefined; const saved = if (settings) |s| s.load(file, &buf) else null; const scale = std.fmt.parseFloat(f32, mem.trim(u8, saved orelse "1", " \n")) catch 1; return .{ .scale = if (math.isFinite(scale)) math.clamp(scale, min, max) else 1, .settings = settings }; } pub fn set(self: *Zoom, scale: f32) void { self.scale = math.clamp(scale, min, max); } /// Saves the scale, once as the app quits rather than each frame of a /// pinch. pub fn save(self: *const Zoom) void { const s = self.settings orelse return; var buf: [16]u8 = undefined; s.save(file, mem.print(&buf, "{d:.2}\n", .{self.scale}) catch return); } /// The next stop larger or smaller than the scale, which a pinch may have /// left between two. pub fn step(self: *const Zoom, direction: enum { larger, smaller }) f32 { const slack = 0.005; return switch (direction) { .larger => for (steps) |s| { if (s > self.scale + slack) break s; } else max, .smaller => for (0..steps.len) |i| { const s = steps[steps.len - 1 - i]; if (s < self.scale - slack) break s; } else min, }; } test "steps go to the next stop past a scale between two, and stop at the ends" { var zoom: Zoom = .{ .scale = 1, .settings = null }; try testing.expectEqual(1.1, zoom.step(.larger)); try testing.expectEqual(0.9, zoom.step(.smaller)); zoom.set(1.3); try testing.expectEqual(1.5, zoom.step(.larger)); try testing.expectEqual(1.25, zoom.step(.smaller)); zoom.set(9); try testing.expectEqual(max, zoom.scale); try testing.expectEqual(max, zoom.step(.larger)); try testing.expectEqual(2.5, zoom.step(.smaller)); } test "the scale is saved and read back, and a bad file reads as 1" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); const settings: Settings = .{ .io = testing.io, .dir = tmp.dir }; var zoom = load(settings); try testing.expectEqual(1, zoom.scale); zoom.set(1.25); zoom.save(); try testing.expectEqual(1.25, load(settings).scale); settings.save(file, "huge\n"); try testing.expectEqual(1, load(settings).scale); settings.save(file, "40\n"); try testing.expectEqual(max, load(settings).scale); }