diff --git a/ios/Sources/WebViewController.swift b/ios/Sources/WebViewController.swift index 62f2b0b..b046423 100644 --- a/ios/Sources/WebViewController.swift +++ b/ios/Sources/WebViewController.swift @@ -56,12 +56,92 @@ final class WebViewController: UIViewController, WKNavigationDelegate { self?.setBanner(nil) } cfg.userContentController.add(bridge, name: Bridge.handlerName) + Zoom.install(into: cfg.userContentController) setUpBanner() if ServerSettings.isConfigured { load() } } + // MARK: - how big the page is drawn + + /// The page is sized for a phone and for a browser window, and on a Mac -- where the window + /// is often two thousand points across -- it reads small. Scaling it here rather than asking + /// ipx to carry a text size: the page is right everywhere else, and this is the one place + /// that is wrong. + enum Zoom { + private static let key = "ipx.zoom" + static let steps: [Double] = [1.0, 1.15, 1.25, 1.4, 1.6, 1.8, 2.0] + + /// A Mac starts bigger; a phone and an iPad are already right. + static var normal: Double { + #if targetEnvironment(macCatalyst) + return 1.25 + #else + return 1.0 + #endif + } + + static var current: Double { + get { + let v = UserDefaults.standard.double(forKey: key) + return steps.contains(v) ? v : normal + } + set { UserDefaults.standard.set(newValue, forKey: key) } + } + + /// pageZoom alone spilled the page off the screen: ipx's body is `height:100dvh`, and + /// WebKit measures dvh against the unzoomed window, so the page laid itself out at the + /// window's full size and was then scaled up. A percentage of the root resolves against + /// a box that does know about zoom, which is the whole of the fix. + /// CSS zoom, not WKWebView.pageZoom. pageZoom scales what is drawn but leaves the + /// layout box the size of the window, so ipx's full-height page -- body is 100dvh -- + /// laid itself out at the window's height and then got scaled past the bottom of it. + /// CSS zoom is part of layout, so the page measures itself correctly; body's own width + /// and height are divided back out because its containing block is not zoomed. + static func script(_ zoom: Double) -> String { + let css = zoom == 1 + ? "" + : "body{zoom:\(zoom);width:calc(100%/\(zoom))!important;" + + "height:calc(100dvh/\(zoom))!important}" + return "(function(){var id='ipx-zoom';var s=document.getElementById(id);" + + "if(!s){s=document.createElement('style');s.id=id;" + + "(document.head||document.documentElement).appendChild(s)}" + + "s.textContent=\(css.debugDescription)})()" + } + + static func install(into controller: WKUserContentController) { + controller.addUserScript(WKUserScript(source: script(current), + injectionTime: .atDocumentEnd, + forMainFrameOnly: true)) + } + } + + override var keyCommands: [UIKeyCommand]? { + // The page's own shortcuts are single keys and ignore a modifier, so these do not collide. + [ + UIKeyCommand(title: "Bigger", action: #selector(zoomIn), input: "+", modifierFlags: .command), + UIKeyCommand(title: "Bigger", action: #selector(zoomIn), input: "=", modifierFlags: .command), + UIKeyCommand(title: "Smaller", action: #selector(zoomOut), input: "-", modifierFlags: .command), + UIKeyCommand(title: "Actual Size", action: #selector(zoomReset), input: "0", modifierFlags: .command), + ] + } + + @objc private func zoomIn() { step(+1) } + @objc private func zoomOut() { step(-1) } + @objc private func zoomReset() { apply(Zoom.normal) } + + private func step(_ by: Int) { + let steps = Zoom.steps + let at = steps.firstIndex(of: Zoom.current) ?? 0 + apply(steps[min(steps.count - 1, max(0, at + by))]) + } + + private func apply(_ zoom: Double) { + Zoom.current = zoom + webView.evaluateJavaScript(Zoom.script(zoom)) + } + // MARK: - which server /// Shake to change it. There is nowhere on screen to put a button -- the page fills it, and