Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
68a0ba8
feat(displays): add custom overlay placement mode
esphynox Jul 31, 2026
e32ab3e
feat(displays): add custom keyboard visualizer placement
esphynox Jul 31, 2026
54204cf
feat(displays): add custom keyboard visualizer placement
esphynox Jul 31, 2026
65c072b
feat(displays): sync custom placement with dragged display
esphynox Aug 1, 2026
078b131
feat(displays): persist custom position after dragging
esphynox Aug 1, 2026
f622940
fix(displays): finish custom positioning on pane exit
esphynox Aug 1, 2026
c2641e9
refactor(displays): fold custom placement into anchor picker
esphynox Aug 1, 2026
a67c58b
refactor(keyboard): remove position picker
esphynox Aug 1, 2026
1b4a863
chore(l10n): update custom position labels
esphynox Aug 1, 2026
a446b1f
chore(l10n): localize custom position strings
esphynox Aug 1, 2026
4c9834a
refactor(displays): remove unused placement labels
esphynox Aug 1, 2026
c4303fd
refactor(keyboard): move size into general section
esphynox Aug 1, 2026
c8a1f8f
refactor(foundation): add CGFloat clamp helper
esphynox Aug 1, 2026
e1f7c28
refactor(coregraphics): add CGRect distance helper
esphynox Aug 1, 2026
f6a1abb
refactor(coregraphics): add normalized point helper
esphynox Aug 1, 2026
c2c3cb0
test(extensions): cover geometry helpers
esphynox Aug 1, 2026
bb7a282
refactor(keyboard): nest placement type in controller
esphynox Aug 1, 2026
d0cac05
refactor(keyboard): clarify custom position naming
esphynox Aug 1, 2026
6902343
refactor(keyboard): clarify custom position naming
esphynox Aug 1, 2026
1d4aede
refactor(displays): nest placement selection in view model
esphynox Aug 1, 2026
b6bd979
refactor(keyboard): add custom placement settings helper
esphynox Aug 1, 2026
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
26 changes: 23 additions & 3 deletions Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Sparkle
@MainActor
final class AppUIContainer {
let aboutWindowController: AboutWindowController
let keyboardVisualizerPlacementWindowController: KeyboardVisualizerPlacementWindowController
let permissionsOnboardingWindowController: PermissionsOnboardingWindowController
let settingsWindowController: SettingsWindowController

Expand All @@ -19,19 +20,38 @@ final class AppUIContainer {
services: AppServiceContainer,
updater: SPUUpdater
) {
aboutWindowController = AboutWindowController()
permissionsOnboardingWindowController = PermissionsOnboardingWindowController(
self.aboutWindowController = AboutWindowController()
let keyboardVisualizerPlacementWindowController = KeyboardVisualizerPlacementWindowController(
settings: settings.keyboardVisualizerSettings
)
self.keyboardVisualizerPlacementWindowController = keyboardVisualizerPlacementWindowController
self.permissionsOnboardingWindowController = PermissionsOnboardingWindowController(
permissionsService: services.permissionsService
)
settingsWindowController = SettingsWindowController(
let settingsWindowController = SettingsWindowController(
shortcutManager: services.shortcutManager,
appSettings: settings.appSettings,
pointerRingVisualizer: services.pointerVisualizersManager.ring,
pointerRingSettings: settings.pointerRingSettings,
pointerIconSettings: settings.pointerIconSettings,
keyboardVisualizerSettings: settings.keyboardVisualizerSettings,
startSettingKeyboardVisualizerPosition: { [weak keyboardVisualizerPlacementWindowController] onPlacementChanged in
keyboardVisualizerPlacementWindowController?.startSettingPosition(onPlacementChanged: onPlacementChanged)
},
stopSettingKeyboardVisualizerPosition: { [weak keyboardVisualizerPlacementWindowController] in
keyboardVisualizerPlacementWindowController?.stopSettingPosition()
},
permissionsService: services.permissionsService,
updater: updater
)
settingsWindowController.onClose = { [weak keyboardVisualizerPlacementWindowController, keyboardVisualizerSettings = settings.keyboardVisualizerSettings] in
guard let placement = keyboardVisualizerPlacementWindowController?.stopSettingPosition() else { return }
keyboardVisualizerSettings.applyCustomPlacement(
screenID: placement.screenID,
normalizedX: placement.positionX,
normalizedY: placement.positionY
)
}
self.settingsWindowController = settingsWindowController
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ enum KeyboardKeyCode: UInt16 {
// MARK: - Letters & punctuation

case a = 0x00
case s = 0x01
case t = 0x11
case e = 0x0E
case digit7 = 0x1A
case minus = 0x1B
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ extension DisplaysSettingsPane {
let screens: [Screen]
let selectedScreen: Screen
let anchor: KeyboardVisualizerAnchor
let placementMode: KeyboardVisualizerSettings.PlacementMode
let stackAxis: KeyboardVisualizerStackAxis
let windowPadding: Double
let customPositionNormalizedX: Double
let customPositionNormalizedY: Double
let onSelectScreen: (Screen) -> Void

private let previewHeight = Spacing.grid(48)
Expand Down Expand Up @@ -64,7 +67,7 @@ private extension DisplaysSettingsPane.PreviewCard {
RoundedRectangle(cornerRadius: Radius.sm, style: .continuous)
.fill(Color.Theme.Accent.controlAccent)
.frame(width: markerSize.width, height: markerSize.height)
.offset(self.anchorOffset(in: size, for: screen, markerSize: markerSize))
.offset(self.markerOffset(in: size, for: screen, markerSize: markerSize))
.shadow(color: Color.Theme.Shadow.displayMarker, radius: 6, y: 2)
}
}
Expand Down Expand Up @@ -126,6 +129,15 @@ private extension DisplaysSettingsPane.PreviewCard {
return min(1, max(minimumScale, displayScale))
}

func markerOffset(in size: CGSize, for screen: Screen, markerSize: CGSize) -> CGSize {
switch self.placementMode {
case .anchored:
return self.anchorOffset(in: size, for: screen, markerSize: markerSize)
case .custom:
return self.customPositionOffset(in: size, markerSize: markerSize)
}
}

func anchorOffset(in size: CGSize, for screen: Screen, markerSize: CGSize) -> CGSize {
let halfMarkerWidth = markerSize.width / 2
let halfMarkerHeight = markerSize.height / 2
Expand Down Expand Up @@ -159,6 +171,13 @@ private extension DisplaysSettingsPane.PreviewCard {

return CGSize(width: x, height: y)
}

func customPositionOffset(in size: CGSize, markerSize: CGSize) -> CGSize {
CGSize(
width: size.width * CGFloat(self.customPositionNormalizedX) - size.width / 2,
height: size.height / 2 - size.height * CGFloat(self.customPositionNormalizedY)
)
}
}

// MARK: - Layout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,104 @@ import SwiftUI
struct DisplaysSettingsPane: View {
@StateObject private var model: DisplaysSettingsPaneViewModel

init(keyboardVisualizerSettings: KeyboardVisualizerSettings) {
_model = StateObject(wrappedValue: DisplaysSettingsPaneViewModel(keyboardVisualizerSettings: keyboardVisualizerSettings))
init(model: DisplaysSettingsPaneViewModel) {
_model = StateObject(wrappedValue: model)
}

var body: some View {
DisplaysSettingsPane.PreviewCard(
screens: self.model.screens,
selectedScreen: self.model.selectedScreen,
anchor: self.model.selectedAnchor,
stackAxis: self.model.stackAxis,
windowPadding: self.model.windowPadding,
onSelectScreen: { screen in
self.model.selectedScreenID = screen.id
}
)

SettingsStack {
SettingsSectionView {
SettingsControlRow(
title: L10n.Displays.displayLabel,
subtitle: L10n.Displays.displaySubtitle
) {
Picker("", selection: $model.selectedScreenID) {
ForEach(model.screens) { screen in
Text(screen.displayName).tag(screen.id)
Group {
DisplaysSettingsPane.PreviewCard(
screens: self.model.screens,
selectedScreen: self.model.selectedScreen,
anchor: self.model.selectedAnchor,
placementMode: self.model.placementMode,
stackAxis: self.model.stackAxis,
windowPadding: self.model.windowPadding,
customPositionNormalizedX: self.model.customPositionNormalizedX,
customPositionNormalizedY: self.model.customPositionNormalizedY,
onSelectScreen: { screen in
self.model.selectedScreenID = screen.id
}
)

SettingsStack {
SettingsSectionView {
SettingsControlRow(
title: L10n.Displays.displayLabel,
subtitle: L10n.Displays.displaySubtitle
) {
Picker("", selection: $model.selectedScreenID) {
ForEach(model.screens) { screen in
Text(screen.displayName).tag(screen.id)
}
}
.labelsHidden()
.accessibilityLabel(L10n.Displays.displayLabel)
.frame(width: Size.Control.settingsPickerWidth, alignment: .trailing)
}
.labelsHidden()
.accessibilityLabel(L10n.Displays.displayLabel)
.frame(width: Size.Control.settingsPickerWidth, alignment: .trailing)
}

Divider()
Divider()

SettingsControlRow(
title: L10n.Displays.anchorLabel,
subtitle: L10n.Displays.anchorSubtitle
) {
KeyboardVisualizerAnchorPicker(
selection: $model.selectedAnchor,
accessibilityLabel: L10n.Displays.anchorLabel
)
}
SettingsControlRow(
title: L10n.Displays.anchorLabel,
subtitle: L10n.Displays.anchorSubtitle
) {
Picker("", selection: self.anchorSelection) {
ForEach(DisplaysSettingsPaneViewModel.PlacementSelection.allCases, id: \.self) { selection in
Text(selection.label).tag(selection)
}
}
.labelsHidden()
.accessibilityLabel(L10n.Displays.anchorLabel)
.frame(width: Size.Control.settingsPickerWidth, alignment: .trailing)
}

Divider()
if self.model.isCustomPlacement {
Divider()

SettingsControlRow(
title: L10n.Displays.marginLabel,
subtitle: L10n.Displays.marginSubtitle
) {
SettingsSliderControl(
value: $model.windowPadding,
range: model.paddingRange,
step: model.paddingStep,
accessibilityLabel: L10n.Displays.marginLabel
)
self.customPlacementControls
} else {
Divider()

SettingsControlRow(
title: L10n.Displays.marginLabel,
subtitle: L10n.Displays.marginSubtitle
) {
SettingsSliderControl(
value: self.$model.windowPadding,
range: self.model.paddingRange,
step: self.model.paddingStep,
accessibilityLabel: L10n.Displays.marginLabel
)
}
}
}
}
}
.onDisappear {
self.model.finishCustomPositionSetting()
}
}
}

private extension DisplaysSettingsPane {
var customPlacementControls: some View {
SettingsControlRow(
title: L10n.Displays.CustomPosition.label,
subtitle: L10n.Displays.CustomPosition.setSubtitle
) {
Button(self.model.isSettingCustomPosition ? L10n.Displays.CustomPosition.stopSettingButton : L10n.Displays.CustomPosition.startSettingButton) {
self.model.toggleCustomPositionSetting()
}
.controlSize(.regular)
.frame(width: Size.Control.settingsPickerWidth, alignment: .trailing)
}
}

var anchorSelection: Binding<DisplaysSettingsPaneViewModel.PlacementSelection> {
Binding(
get: { self.model.anchorSelection },
set: { self.model.anchorSelection = $0 }
)
}
}
Loading
Loading