Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions Apps/PointerLocker/Onboarding/WelcomeStep.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import SwiftUI

/// When Welcome's two cards stack instead of sitting side by side: only for
/// accessibility text sizes and narrow windows. Measuring whether the text
/// fits on one line (ViewThatFits) stacked them on a full iPad as soon as a
/// summary grew longer than half the width.
enum WelcomeLayout {
static func stacksCards(dynamicTypeSize: DynamicTypeSize, horizontalSizeClass: UserInterfaceSizeClass?) -> Bool {
dynamicTypeSize.isAccessibilitySize || horizontalSizeClass == .compact
}
}

struct WelcomeStep: View {
let services: [ServiceProfile]
let onContinue: () -> Void
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@Environment(\.horizontalSizeClass) private var horizontalSizeClass

var body: some View {
// Scrolls when the text doesn't fit (a small window at a large text
Expand All @@ -23,12 +35,13 @@ struct WelcomeStep: View {
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 520)
// Play and Create side by side, stacked when the text is large.
// Cards as tall as their text, and as tall as each other.
ViewThatFits(in: .horizontal) {
HStack(alignment: .top, spacing: 16) { worldCards }
VStack(spacing: 16) { worldCards }
}
// Create and Play side by side, stacked when the text is large
// or the window narrow. Cards as tall as their text, and as
// tall as each other.
let layout = WelcomeLayout.stacksCards(dynamicTypeSize: dynamicTypeSize, horizontalSizeClass: horizontalSizeClass)
? AnyLayout(VStackLayout(spacing: 16))
: AnyLayout(HStackLayout(alignment: .top, spacing: 16))
layout { worldCards }
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 640)
}
Expand Down
10 changes: 8 additions & 2 deletions Apps/PointerLocker/Services/FigmaProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ extension ServiceProfile {
colors: (UIColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1), UIColor(red: 0.13, green: 0.07, blue: 0.27, alpha: 1))
),
setupTips: [
SetupTip(
id: "navigate",
symbol: "arrow.up.and.down.and.arrow.left.and.right",
title: "Use Figma as on a computer",
detail: "Space + drag pans, ⌘ + scroll zooms and Shift + scroll moves sideways. Right-click and keyboard shortcuts work too."
),
SetupTip(
id: "scrub",
symbol: "arrow.left.and.right",
title: "Drag a number's label to change it",
detail: "The pointer locks while you drag, so the value keeps changing past the edge of the screen. Space + drag moves the canvas."
detail: "The pointer locks while you drag, so the value keeps changing past the edge of the screen."
),
SetupTip(
id: "session",
Expand All @@ -36,7 +42,7 @@ extension ServiceProfile {
),
],
wording: ServiceWording(
mouseNeeded: "Figma needs a mouse or trackpad to drag and scrub values. Touch alone won't work.",
mouseNeeded: "Figma needs a mouse or trackpad to pan, right-click and drag values. Touch alone won't work.",
mouseTest: "Move it and watch the check mark. If it reacts, Figma can see the mouse too.",
keyboardReady: "Ready for shortcuts.",
lockReady: "The pointer can lock while you drag.",
Expand Down
8 changes: 4 additions & 4 deletions Apps/PointerLocker/Services/ServiceCategory.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// The two worlds Mouselook serves, shown side by side on Welcome and as
/// headings in the chooser: cloud games, and design and 3D tools.
/// headings in the chooser: design and 3D tools first, then cloud games.
enum ServiceCategory: CaseIterable {
case play, create
case create, play

var title: String {
switch self {
Expand All @@ -14,7 +14,7 @@ enum ServiceCategory: CaseIterable {
var summary: String {
switch self {
case .play: "Aim and look around in cloud games."
case .create: "Drag, scrub and pan in design tools."
case .create: "Pan, zoom and use shortcuts in design tools."
}
}

Expand Down Expand Up @@ -42,7 +42,7 @@ enum ServiceCategory: CaseIterable {
services.filter { $0.category == self }.map(\.name).joined(separator: ", ")
}

/// Services grouped by world, Play first; worlds without services are left out.
/// Services grouped by world, Create first; worlds without services are left out.
static func groups(of services: [ServiceProfile]) -> [(category: ServiceCategory, services: [ServiceProfile])] {
allCases.compactMap { category in
let members = services.filter { $0.category == category }
Expand Down
2 changes: 1 addition & 1 deletion Apps/PointerLocker/Services/ServiceProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct ServiceProfile {
static let all: [ServiceProfile] = [.geforceNow, .figma, .generic]

/// What onboarding and Settings offer. `generic` stays internal.
static let selectable: [ServiceProfile] = [.geforceNow, .figma]
static let selectable: [ServiceProfile] = [.figma, .geforceNow]

static func profile(id: String) -> ServiceProfile? {
all.first { $0.id == id }
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ An iPad browser, built on the system WKWebView, that gives web pages a working
**Pointer Lock API**, so a mouse works the way it does on a computer. Safari and
WebKit on iPadOS don't implement pointer lock. Two kinds of service use it:

- **Create:** design tools such as Figma, for dragging number fields past the
screen edge. Figma sees a desktop browser, so its keyboard shortcuts work
too: Space + drag pans the canvas, ⌘ + scroll zooms. More browser-based
design tools will follow as they're tested.
- **Create:** design tools such as Figma, used the way you use them on a
computer. Figma sees a desktop browser, and Mouselook passes along the
modifier keys iPadOS drops from scrolling, so Space + drag pans, ⌘ + scroll
zooms, Shift + scroll moves sideways, right-click opens context menus, and
keyboard shortcuts and modifier clicks (Shift + click, ⌥ + drag) work.
Pointer lock lets you drag number fields past the screen edge. More
browser-based design tools will follow as they're tested.
- **Play:** cloud games such as GeForce NOW (`play.geforcenow.com`), for
mouse-look: aiming and looking around. Other browser-based services that
use pointer lock, such as Xbox Cloud Gaming, should work through ⋯ ▸ Open
Expand Down
21 changes: 13 additions & 8 deletions docs/app-store/review-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,26 @@ Apple ID emails from those messages as external testers.

**Beta App Description**

Mouselook lets web apps on iPad use the mouse the way they do on a desktop:
dragging number fields in Figma keeps going past the screen edge, and games in
GeForce NOW can capture the mouse for camera control. Needs an iPad with a
Mouselook lets you use Figma on iPad the way you do on a computer: Space + drag
pans, ⌘ + scroll zooms, right-click and keyboard shortcuts work, and dragging a
number field keeps going past the screen edge. Games in GeForce NOW can capture
the mouse for camera control. Needs an iPad with a
mouse or trackpad and a keyboard.

**What to Test**

1. Pick Figma in onboarding, sign in, open one of your files.
2. Drag a number field label (X, Y, W, rotation) past the screen edge. The
2. Hold Space and drag to pan, hold ⌘ and scroll to zoom, and hold Shift and
scroll to move sideways.
3. Use the keyboard and mouse as on a computer: V, R, T, ⌘D, ⌘G, right-click
for the context menu, Shift + click to select several layers, ⌥ + drag to
duplicate.
4. Drag a number field label (X, Y, W, rotation) past the screen edge. The
value keeps changing instead of stopping at the edge.
3. Hold ⌘ and scroll to zoom. Switch to GeForce NOW from the ⋯ menu and back;
the Figma file reopens.
4. In GeForce NOW, start a game and click into it. The mouse turns the camera
5. Switch to GeForce NOW from the ⋯ menu and back; the Figma file reopens.
6. In GeForce NOW, start a game and click into it. The mouse turns the camera
until you hold Esc for a second.
5. If something doesn't work: ⋯ ▸ Settings ▸ Copy diagnostics, and send it
7. If something doesn't work: ⋯ ▸ Settings ▸ Copy diagnostics, and send it
with your feedback.

**Feedback email:** mouselook@icebear.sk
Expand Down
2 changes: 1 addition & 1 deletion project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ settings:
# Change to your own team to build elsewhere.
DEVELOPMENT_TEAM: AR9ZHABG8Y # IceBear s.r.o.
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
CURRENT_PROJECT_VERSION: "3"

targets:
PointerLocker:
Expand Down
9 changes: 5 additions & 4 deletions tests/PointerLockerTests/ServiceCategoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ final class ServiceCategoryTests: XCTestCase {
XCTAssertEqual(ServiceProfile.figma.category, .create)
}

func testTheChooserGroupsPlayBeforeCreateAndHidesEmptyGroups() {
let groups = ServiceCategory.groups(of: [.figma, .geforceNow])
XCTAssertEqual(groups.map(\.category), [.play, .create])
XCTAssertEqual(groups.map { $0.services.map(\.id) }, [["geforcenow"], ["figma"]])
func testCreateLeadsOnWelcomeAndInTheChooser() {
XCTAssertEqual(ServiceCategory.allCases, [.create, .play])
let groups = ServiceCategory.groups(of: [.geforceNow, .figma])
XCTAssertEqual(groups.map(\.category), [.create, .play])
XCTAssertEqual(groups.map { $0.services.map(\.id) }, [["figma"], ["geforcenow"]])
XCTAssertEqual(ServiceCategory.groups(of: [.figma]).map(\.category), [.create], "no empty Play heading")
}

Expand Down
8 changes: 4 additions & 4 deletions tests/PointerLockerTests/ServiceMenuTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ final class ServiceMenuTests: XCTestCase {

func testTheMenuListsTheServicesAndMarksTheCurrentOne() {
let entries = ServiceSwitch.menuEntries(current: "figma")
XCTAssertEqual(entries.map(\.name), ["GeForce NOW", "Figma"])
XCTAssertEqual(entries.map(\.subtitle), ["Play", "Create"])
XCTAssertEqual(entries.map(\.isCurrent), [false, true])
XCTAssertEqual(entries.map(\.symbol), [ServiceProfile.geforceNow.artwork.symbol, ServiceProfile.figma.artwork.symbol])
XCTAssertEqual(entries.map(\.name), ["Figma", "GeForce NOW"])
XCTAssertEqual(entries.map(\.subtitle), ["Create", "Play"])
XCTAssertEqual(entries.map(\.isCurrent), [true, false])
XCTAssertEqual(entries.map(\.symbol), [ServiceProfile.figma.artwork.symbol, ServiceProfile.geforceNow.artwork.symbol])
}

func testNoServicesSectionWithOnlyOneService() {
Expand Down
8 changes: 6 additions & 2 deletions tests/PointerLockerTests/ServicePresentationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import XCTest
@testable import PointerLocker

final class ServicePresentationTests: XCTestCase {
func testOnboardingOffersGeForceNowAndFigmaButNotTheGenericProfile() {
XCTAssertEqual(ServiceProfile.selectable.map(\.id), ["geforcenow", "figma"])
func testOnboardingOffersFigmaThenGeForceNowButNotTheGenericProfile() {
XCTAssertEqual(ServiceProfile.selectable.map(\.id), ["figma", "geforcenow"])
}

func testEverySelectableServiceCanBePresented() {
Expand All @@ -27,6 +27,10 @@ final class ServicePresentationTests: XCTestCase {
XCTAssertEqual(tip?.title, "Set the stream to 1920×1080")
}

func testFigmaLeadsWithDesktopNavigation() {
XCTAssertEqual(ServiceProfile.figma.setupTips.first?.id, "navigate")
}

func testFigmaTellsYouHowToScrub() {
let tip = ServiceProfile.figma.setupTips.first { $0.id == "scrub" }
XCTAssertEqual(tip?.title, "Drag a number's label to change it")
Expand Down
18 changes: 18 additions & 0 deletions tests/PointerLockerTests/WelcomeLayoutTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import SwiftUI
import XCTest
@testable import PointerLocker

final class WelcomeLayoutTests: XCTestCase {
func testCardsSitSideBySideOnAFullIPadAtLargeTextSizes() {
XCTAssertFalse(WelcomeLayout.stacksCards(dynamicTypeSize: .large, horizontalSizeClass: .regular))
XCTAssertFalse(WelcomeLayout.stacksCards(dynamicTypeSize: .xxxLarge, horizontalSizeClass: .regular))
}

func testCardsStackAtAccessibilityTextSizes() {
XCTAssertTrue(WelcomeLayout.stacksCards(dynamicTypeSize: .accessibility1, horizontalSizeClass: .regular))
}

func testCardsStackInANarrowWindow() {
XCTAssertTrue(WelcomeLayout.stacksCards(dynamicTypeSize: .large, horizontalSizeClass: .compact))
}
}
Loading